feat(redis): enable remote terminal through tracked background tasks

This commit is contained in:
guoyuqi
2026-10-09 17:59:09 +08:00
parent c329c52c6b
commit 22abdf9e79
7 changed files with 55 additions and 176 deletions
+1 -16
View File
@@ -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
-7
View File
@@ -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
+30 -43
View File
@@ -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 {
+1 -18
View File
@@ -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": {
+1 -18
View File
@@ -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": {
+1 -1
View File
@@ -198,7 +198,7 @@ export const loadRedisCliStatus = (node: string) => {
);
};
export const installRedisCli = (taskID: string, node: string) => {
return http.post<Database.RedisCliStatus>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
return http.post<string>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
taskID,
});
};
+21 -73
View File
@@ -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')
}}
</el-button>
<el-button
v-if="
currentDB.from === 'remote' &&
cliTaskID &&
(cliInstalling || redisCliExist || cliTaskStatus === 'Failed')
"
link
type="primary"
@click="openCliTaskLog"
>
{{ $t('commons.button.log') }}
{{ $t('commons.button.enable') }}
</el-button>
</el-empty>
<div>
@@ -159,7 +140,7 @@
</DialogPro>
<QuickCmd ref="dialogQuickCmdRef" @reload="loadQuickCmd" />
<TaskLog ref="cliTaskLogRef" />
<TaskLog ref="cliTaskLogRef" @close="checkCliValid" />
</div>
</template>
@@ -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<InstanceType<typeof TaskLog>>();
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<typeof setTimeout>;
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);
});
</script>