From 22abdf9e796e264b65174451ed6413394deab114 Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Fri, 9 Oct 2026 15:20:00 +0800 Subject: [PATCH] feat(redis): enable remote terminal through tracked background tasks --- agent/app/api/v2/database_redis.go | 17 +--- agent/app/dto/database.go | 7 -- agent/app/service/database_redis.go | 73 +++++++--------- core/cmd/server/docs/docs.go | 19 +---- core/cmd/server/docs/swagger.json | 19 +---- frontend/src/api/modules/database.ts | 2 +- frontend/src/views/database/redis/index.vue | 94 +++++---------------- 7 files changed, 55 insertions(+), 176 deletions(-) diff --git a/agent/app/api/v2/database_redis.go b/agent/app/api/v2/database_redis.go index f3717e9c7..a2a01e3e7 100644 --- a/agent/app/api/v2/database_redis.go +++ b/agent/app/api/v2/database_redis.go @@ -88,7 +88,7 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) { // @Summary Install redis-cli // @Accept json // @Param request body dto.RedisCliInstall true "request" -// @Success 200 {object} dto.RedisCliStatus +// @Success 200 {string} string // @Security ApiKeyAuth // @Security Timestamp // @Router /databases/redis/install/cli [post] @@ -105,21 +105,6 @@ func (b *BaseApi) InstallCli(c *gin.Context) { helper.SuccessWithData(c, data) } -// @Tags Database Redis -// @Summary Load redis-cli installation status -// @Success 200 {object} dto.RedisCliStatus -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /databases/redis/cli/status [get] -func (b *BaseApi) LoadRedisCliStatus(c *gin.Context) { - data, err := redisService.LoadCliStatus() - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, data) -} - // @Tags Database Redis // @Summary Update redis conf // @Accept json diff --git a/agent/app/dto/database.go b/agent/app/dto/database.go index e9f5984e8..945eb2a6b 100644 --- a/agent/app/dto/database.go +++ b/agent/app/dto/database.go @@ -26,13 +26,6 @@ type RedisCliInstall struct { TaskID string `json:"taskID" validate:"omitempty,uuid"` } -type RedisCliStatus struct { - Installed bool `json:"installed"` - TaskID string `json:"taskID"` - Status string `json:"status"` - ErrorMsg string `json:"errorMsg"` -} - // mysql type MysqlDBSearch struct { PageInfo diff --git a/agent/app/service/database_redis.go b/agent/app/service/database_redis.go index 4d2611108..822b80783 100644 --- a/agent/app/service/database_redis.go +++ b/agent/app/service/database_redis.go @@ -29,8 +29,6 @@ import ( type RedisService struct{} -const redisCliTaskName = "RedisCliEnable" - // The CLI container is shared by all remote Redis databases on this node. var redisCliInstallMutex sync.Mutex @@ -44,8 +42,7 @@ type IRedisService interface { LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error) CheckHasCli() bool - InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error) - LoadCliStatus() (*dto.RedisCliStatus, error) + InstallCli(req dto.RedisCliInstall) (string, error) } func NewIRedisService() IRedisService { @@ -93,55 +90,45 @@ func (u *RedisService) checkCliInstalled() (bool, error) { return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil } -func (u *RedisService) LoadCliStatus() (*dto.RedisCliStatus, error) { - latest, err := taskRepo.GetFirst(repo.WithByName(redisCliTaskName), repo.WithByType(task.TaskScopeContainer), repo.WithOrderDesc("created_at")) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - installed, err := u.checkCliInstalled() - if err != nil { - return nil, err - } - return &dto.RedisCliStatus{ - Installed: installed, - TaskID: latest.ID, - Status: latest.Status, - ErrorMsg: latest.ErrorMsg, - }, nil -} - -func (u *RedisService) InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error) { +func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) { if !redisCliInstallMutex.TryLock() { - return nil, buserr.New("TaskIsExecuting") + return "", buserr.New("TaskIsExecuting") } defer redisCliInstallMutex.Unlock() - status, err := u.LoadCliStatus() - if err != nil { - return nil, err - } - if status.Status == constant.StatusExecuting || status.Installed { - return status, nil - } - if req.TaskID == "" { - req.TaskID = uuid.NewString() - } - // Never reuse an existing task ID: doing so would truncate its log. - if _, err := taskRepo.GetFirst(taskRepo.WithByID(req.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) { - if err != nil { - return nil, err - } - return nil, buserr.New("TaskIsExecuting") - } item := dto.ContainerOperate{ TaskID: req.TaskID, Name: "1Panel-redis-cli-tools", Image: "redis:7.4.4", Networks: []dto.ContainerNetwork{{Network: "1panel-network"}}, } - if err := (&ContainerService{}).containerCreate(item, true, redisCliTaskName); err != nil { - return nil, err + running, err := taskRepo.GetFirst( + repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)), + repo.WithByType(task.TaskScopeContainer), + taskRepo.WithByStatus(constant.StatusExecuting), + ) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return "", err } - return &dto.RedisCliStatus{TaskID: req.TaskID, Status: constant.StatusExecuting}, nil + if running.ID != "" { + return running.ID, nil + } + installed, err := u.checkCliInstalled() + if err != nil || installed { + return "", err + } + if item.TaskID == "" { + item.TaskID = uuid.NewString() + } + if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) { + if err != nil { + return "", err + } + return "", buserr.New("TaskIsExecuting") + } + if err := NewIContainerService().ContainerCreate(item, true); err != nil { + return "", err + } + return item.TaskID, nil } func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index aea6d2fb9..2acd343ff 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -15134,7 +15134,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/dto.RedisCliStatus" + "type": "string" } } }, @@ -40201,23 +40201,6 @@ const docTemplate = `{ } } }, - "dto.RedisCliStatus": { - "type": "object", - "properties": { - "errorMsg": { - "type": "string" - }, - "installed": { - "type": "boolean" - }, - "status": { - "type": "string" - }, - "taskID": { - "type": "string" - } - } - }, "dto.RedisConf": { "properties": { "containerName": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index eff12ec2a..77719e37f 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -15130,7 +15130,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/dto.RedisCliStatus" + "type": "string" } } }, @@ -40197,23 +40197,6 @@ } } }, - "dto.RedisCliStatus": { - "type": "object", - "properties": { - "errorMsg": { - "type": "string" - }, - "installed": { - "type": "boolean" - }, - "status": { - "type": "string" - }, - "taskID": { - "type": "string" - } - } - }, "dto.RedisConf": { "properties": { "containerName": { diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index 3f1c1fd3f..463ad6576 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -198,7 +198,7 @@ export const loadRedisCliStatus = (node: string) => { ); }; export const installRedisCli = (taskID: string, node: string) => { - return http.post(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, { + return http.post(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, { taskID, }); }; diff --git a/frontend/src/views/database/redis/index.vue b/frontend/src/views/database/redis/index.vue index b0133e288..132847100 100644 --- a/frontend/src/views/database/redis/index.vue +++ b/frontend/src/views/database/redis/index.vue @@ -86,28 +86,9 @@ v-permission type="primary" :loading="cliSubmitting" - :disabled="(!cliStatusLoaded || cliInstalling) && !cliPollFailed" @click="installCli" > - {{ - cliPollFailed - ? $t('commons.button.retry') - : cliInstalling - ? $t('database.redisCliEnabling') - : $t('commons.button.enable') - }} - - - {{ $t('commons.button.log') }} + {{ $t('commons.button.enable') }}
@@ -159,7 +140,7 @@ - +
@@ -174,14 +155,12 @@ import { checkAppInstalled } from '@/api/modules/app'; import { useGlobalStore } from '@/composables/useGlobalStore'; import { listDatabases, loadRedisCliStatus, installRedisCli } from '@/api/modules/database'; import { Database } from '@/api/interface/database'; -import { MsgError, MsgSuccess } from '@/utils/message'; import i18n from '@/lang'; import { getCommandList } from '@/api/modules/command'; import { routerToName, routerToNameWithQuery } from '@/utils/router'; import { useOperateNodeContext } from '@/composables/useOperateNodeContext'; import TaskLog from '@/components/log/task/index.vue'; -import bus from '@/global/bus'; import { newUUID } from '@/utils/id'; const { currentNode, currentRedisDB, openMenuTabs } = useGlobalStore(); @@ -202,21 +181,8 @@ const open = ref(false); const redisCliExist = ref(false); const cliSubmitting = ref(false); -const cliStatusLoaded = ref(false); -const cliPollFailed = ref(false); -const cliTaskID = ref(''); -const cliTaskStatus = ref(''); const cliTaskLogRef = ref>(); -const cliInstalling = computed(() => cliTaskStatus.value === 'Executing'); -const cliStatusText = computed(() => { - if (cliPollFailed.value) return i18n.global.t('commons.res.commonError'); - if (cliInstalling.value) return i18n.global.t('database.redisCliEnabling'); - if (cliTaskStatus.value === 'Failed') return i18n.global.t('database.redisCliEnableFailed'); - return redisCliExist.value ? i18n.global.t('database.redisCliEnabled') : i18n.global.t('database.redisCliHelper'); -}); -let cliPollTimer: ReturnType; -let cliGeneration = 0; -let cliPollFailures = 0; +const operateNode = currentNode.value; let disposed = false; const appKey = ref('redis'); @@ -298,7 +264,7 @@ const loadDBOptions = async () => { const generation = cliGeneration; try { const res = await listDatabases('redis,redis-cluster'); - if (disposed || generation !== cliGeneration) return; + if (disposed || operateNode !== currentNode.value) return; let datas = res.data || []; dbOptionsLocal.value = []; dbOptionsRemote.value = []; @@ -335,7 +301,7 @@ const loadDBOptions = async () => { reOpenTerminal(); } } finally { - if (!disposed && generation === cliGeneration) isLoaded.value = true; + if (!disposed && operateNode === currentNode.value) isLoaded.value = true; } }; @@ -351,9 +317,8 @@ const reOpenTerminal = async () => { const initTerminal = async () => { if (disposed || !currentDB.value) return; - const generation = cliGeneration; const database = currentDBName.value; - const isCurrent = () => !disposed && generation === cliGeneration && database === currentDBName.value; + const isCurrent = () => !disposed && operateNode === currentNode.value && database === currentDBName.value; loading.value = true; if (currentDB.value.from === 'remote') { if (!redisCliExist.value) { @@ -461,44 +426,29 @@ const refreshCliStatus = async (generation: number) => { }; const checkCliValid = async () => { - const generation = cliGeneration; - await refreshCliStatus(generation); - if (!disposed && generation === cliGeneration) await loadDBOptions(); + if (disposed || operateNode !== currentNode.value) return; + try { + const res = await checkRedisCli(); + if (disposed || operateNode !== currentNode.value) return; + redisCliExist.value = res.data; + } finally { + if (!disposed && operateNode === currentNode.value) await loadDBOptions(); + } }; const installCli = async () => { if (cliSubmitting.value) return; - if (cliPollFailed.value) { - cliPollFailures = 0; - cliPollFailed.value = false; - cliSubmitting.value = true; - const generation = cliGeneration; - try { - await refreshCliStatus(generation); - } finally { - if (!disposed && generation === cliGeneration) cliSubmitting.value = false; - } - return; - } - if (cliInstalling.value || !cliStatusLoaded.value) return; - const generation = cliGeneration; - const node = currentNode.value; cliSubmitting.value = true; - clearTimeout(cliPollTimer); try { - const res = await installRedisCli(newUUID(), node); - if (disposed || generation !== cliGeneration) return; - applyCliStatus(res.data); - if (cliInstalling.value) { - MsgSuccess(i18n.global.t('database.redisCliTaskSubmitted')); - openCliTaskLog(); - scheduleCliPoll(generation); + const res = await installRedisCli(newUUID(), operateNode); + if (disposed || operateNode !== currentNode.value) return; + if (res.data) { + cliTaskLogRef.value?.openWithTaskID(res.data, true, operateNode); + } else { + await checkCliValid(); } - } catch { - // The server may have accepted the task even if the response was lost. - if (!disposed && generation === cliGeneration) await refreshCliStatus(generation); } finally { - if (!disposed && generation === cliGeneration) cliSubmitting.value = false; + cliSubmitting.value = false; } }; @@ -551,8 +501,6 @@ const onAfter = () => { }; onBeforeUnmount(() => { disposed = true; - cliGeneration++; - clearTimeout(cliPollTimer); closeTerminal(false); });