mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-13 08:00:35 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fff7bccdd | ||
|
|
c6e0b61720 | ||
|
|
22abdf9e79 | ||
|
|
c329c52c6b |
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
// @Summary Install redis-cli
|
// @Summary Install redis-cli
|
||||||
// @Success 200
|
// @Accept json
|
||||||
|
// @Param request body dto.RedisCliInstall true "request"
|
||||||
|
// @Success 200 {string} string
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/redis/install/cli [post]
|
// @Router /databases/redis/install/cli [post]
|
||||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||||
if err := redisService.InstallCli(); err != nil {
|
var req dto.RedisCliInstall
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := redisService.InstallCli(req)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
|||||||
Port int64 `json:"port"`
|
Port int64 `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisCliInstall struct {
|
||||||
|
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
// mysql
|
// mysql
|
||||||
type MysqlDBSearch struct {
|
type MysqlDBSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -17,12 +23,15 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
"github.com/docker/docker/api/types/container"
|
dockerclient "github.com/docker/docker/client"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisService struct{}
|
type RedisService struct{}
|
||||||
|
|
||||||
|
// The CLI container is shared by all remote Redis databases on this node.
|
||||||
|
var redisCliInstallMutex sync.Mutex
|
||||||
|
|
||||||
type IRedisService interface {
|
type IRedisService interface {
|
||||||
UpdateConf(req dto.RedisConfUpdate) error
|
UpdateConf(req dto.RedisConfUpdate) error
|
||||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
|||||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||||
|
|
||||||
CheckHasCli() bool
|
CheckHasCli() bool
|
||||||
InstallCli() error
|
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRedisService() IRedisService {
|
func NewIRedisService() IRedisService {
|
||||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) CheckHasCli() bool {
|
func (u *RedisService) CheckHasCli() bool {
|
||||||
client, err := docker.NewDockerClient()
|
installed, _ := u.checkCliInstalled()
|
||||||
if err != nil {
|
return installed
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range containerLists {
|
|
||||||
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) InstallCli() error {
|
func (u *RedisService) checkCliInstalled() (bool, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
|
||||||
|
if dockerclient.IsErrNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
|
||||||
|
if !redisCliInstallMutex.TryLock() {
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer redisCliInstallMutex.Unlock()
|
||||||
item := dto.ContainerOperate{
|
item := dto.ContainerOperate{
|
||||||
|
TaskID: req.TaskID,
|
||||||
Name: "1Panel-redis-cli-tools",
|
Name: "1Panel-redis-cli-tools",
|
||||||
Image: "redis:7.4.4",
|
Image: "redis:7.4.4",
|
||||||
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
||||||
}
|
}
|
||||||
return NewIContainerService().ContainerCreate(item, false)
|
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
|
||||||
|
}
|
||||||
|
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 {
|
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
||||||
|
|||||||
@@ -15092,9 +15092,26 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"/databases/redis/install/cli": {
|
"/databases/redis/install/cli": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"in": "body",
|
||||||
|
"name": "request",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.RedisCliInstall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -40152,6 +40169,14 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"dto.RedisCliInstall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.RedisConf": {
|
"dto.RedisConf": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"containerName": {
|
"containerName": {
|
||||||
|
|||||||
@@ -15088,9 +15088,26 @@
|
|||||||
},
|
},
|
||||||
"/databases/redis/install/cli": {
|
"/databases/redis/install/cli": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"in": "body",
|
||||||
|
"name": "request",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.RedisCliInstall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -40148,6 +40165,14 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"dto.RedisCliInstall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.RedisConf": {
|
"dto.RedisConf": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"containerName": {
|
"containerName": {
|
||||||
|
|||||||
@@ -193,8 +193,10 @@ export const redisPersistenceConf = (type: string, database: string) => {
|
|||||||
export const checkRedisCli = () => {
|
export const checkRedisCli = () => {
|
||||||
return http.get<boolean>(`/databases/redis/check`);
|
return http.get<boolean>(`/databases/redis/check`);
|
||||||
};
|
};
|
||||||
export const installRedisCli = () => {
|
export const installRedisCli = (taskID: string, node: string) => {
|
||||||
return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M);
|
return http.post<string>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
|
||||||
|
taskID,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
export const changeRedisPassword = (database: string, password: string) => {
|
export const changeRedisPassword = (database: string, password: string) => {
|
||||||
if (password) {
|
if (password) {
|
||||||
|
|||||||
@@ -81,7 +81,13 @@
|
|||||||
:style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }"
|
:style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }"
|
||||||
:description="loadErrMsg()"
|
:description="loadErrMsg()"
|
||||||
>
|
>
|
||||||
<el-button v-if="currentDB.from === 'remote'" v-permission type="primary" @click="installCli">
|
<el-button
|
||||||
|
v-if="currentDB.from === 'remote'"
|
||||||
|
v-permission
|
||||||
|
type="primary"
|
||||||
|
:loading="cliSubmitting"
|
||||||
|
@click="installCli"
|
||||||
|
>
|
||||||
{{ $t('commons.button.enable') }}
|
{{ $t('commons.button.enable') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-empty>
|
</el-empty>
|
||||||
@@ -134,6 +140,7 @@
|
|||||||
</DialogPro>
|
</DialogPro>
|
||||||
|
|
||||||
<QuickCmd ref="dialogQuickCmdRef" @reload="loadQuickCmd" />
|
<QuickCmd ref="dialogQuickCmdRef" @reload="loadQuickCmd" />
|
||||||
|
<TaskLog ref="cliTaskLogRef" @close="checkCliValid" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -148,12 +155,14 @@ import { checkAppInstalled } from '@/api/modules/app';
|
|||||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||||
import { listDatabases, checkRedisCli, installRedisCli } from '@/api/modules/database';
|
import { listDatabases, checkRedisCli, installRedisCli } from '@/api/modules/database';
|
||||||
import { Database } from '@/api/interface/database';
|
import { Database } from '@/api/interface/database';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { getCommandList } from '@/api/modules/command';
|
import { getCommandList } from '@/api/modules/command';
|
||||||
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
||||||
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
|
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
|
||||||
|
|
||||||
|
import TaskLog from '@/components/log/task/index.vue';
|
||||||
|
import { newUUID } from '@/utils/id';
|
||||||
|
|
||||||
const { currentNode, currentRedisDB, openMenuTabs } = useGlobalStore();
|
const { currentNode, currentRedisDB, openMenuTabs } = useGlobalStore();
|
||||||
useOperateNodeContext(currentNode);
|
useOperateNodeContext(currentNode);
|
||||||
|
|
||||||
@@ -170,7 +179,11 @@ const appStatusRef = ref();
|
|||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
|
|
||||||
const redisCliExist = ref();
|
const redisCliExist = ref(false);
|
||||||
|
const cliSubmitting = ref(false);
|
||||||
|
const cliTaskLogRef = ref<InstanceType<typeof TaskLog>>();
|
||||||
|
const operateNode = currentNode.value;
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
const appKey = ref('redis');
|
const appKey = ref('redis');
|
||||||
const appName = ref();
|
const appName = ref();
|
||||||
@@ -250,6 +263,7 @@ const changeDatabase = async () => {
|
|||||||
const loadDBOptions = async () => {
|
const loadDBOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await listDatabases('redis,redis-cluster');
|
const res = await listDatabases('redis,redis-cluster');
|
||||||
|
if (disposed || operateNode !== currentNode.value) return;
|
||||||
let datas = res.data || [];
|
let datas = res.data || [];
|
||||||
dbOptionsLocal.value = [];
|
dbOptionsLocal.value = [];
|
||||||
dbOptionsRemote.value = [];
|
dbOptionsRemote.value = [];
|
||||||
@@ -286,7 +300,7 @@ const loadDBOptions = async () => {
|
|||||||
reOpenTerminal();
|
reOpenTerminal();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoaded.value = true;
|
if (!disposed && operateNode === currentNode.value) isLoaded.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,6 +315,9 @@ const reOpenTerminal = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initTerminal = async () => {
|
const initTerminal = async () => {
|
||||||
|
if (disposed || !currentDB.value) return;
|
||||||
|
const database = currentDBName.value;
|
||||||
|
const isCurrent = () => !disposed && operateNode === currentNode.value && database === currentDBName.value;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (currentDB.value.from === 'remote') {
|
if (currentDB.value.from === 'remote') {
|
||||||
if (!redisCliExist.value) {
|
if (!redisCliExist.value) {
|
||||||
@@ -311,9 +328,10 @@ const initTerminal = async () => {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
redisIsExist.value = true;
|
redisIsExist.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
terminalShow.value = true;
|
terminalShow.value = true;
|
||||||
redisStatus.value = 'Running';
|
redisStatus.value = 'Running';
|
||||||
terminalRef.value.acceptParams({
|
terminalRef.value?.acceptParams({
|
||||||
endpoint: '/api/v2/hosts/terminal/container',
|
endpoint: '/api/v2/hosts/terminal/container',
|
||||||
args: `source=redis&name=${currentDBName.value}&from=${currentDB.value.from}`,
|
args: `source=redis&name=${currentDBName.value}&from=${currentDB.value.from}`,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -325,13 +343,15 @@ const initTerminal = async () => {
|
|||||||
}
|
}
|
||||||
await checkAppInstalled(currentDB.value.type, currentDBName.value)
|
await checkAppInstalled(currentDB.value.type, currentDBName.value)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
redisIsExist.value = res.data.isExist;
|
redisIsExist.value = res.data.isExist;
|
||||||
redisStatus.value = res.data.status;
|
redisStatus.value = res.data.status;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
if (res.data.status === 'Running') {
|
if (res.data.status === 'Running') {
|
||||||
terminalShow.value = true;
|
terminalShow.value = true;
|
||||||
terminalRef.value.acceptParams({
|
terminalRef.value?.acceptParams({
|
||||||
endpoint: '/api/v2/hosts/terminal/container',
|
endpoint: '/api/v2/hosts/terminal/container',
|
||||||
args: `source=${currentDB.value.type}&name=${currentDBName.value}&from=${currentDB.value.from}`,
|
args: `source=${currentDB.value.type}&name=${currentDBName.value}&from=${currentDB.value.from}`,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -342,6 +362,7 @@ const initTerminal = async () => {
|
|||||||
isRefresh.value = !isRefresh.value;
|
isRefresh.value = !isRefresh.value;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
closeTerminal(false);
|
closeTerminal(false);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
@@ -353,27 +374,30 @@ const closeTerminal = async (isKeepShow: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkCliValid = async () => {
|
const checkCliValid = async () => {
|
||||||
await checkRedisCli()
|
if (disposed || operateNode !== currentNode.value) return;
|
||||||
.then((res) => {
|
try {
|
||||||
redisCliExist.value = res.data;
|
const res = await checkRedisCli();
|
||||||
loadDBOptions();
|
if (disposed || operateNode !== currentNode.value) return;
|
||||||
})
|
redisCliExist.value = res.data;
|
||||||
.catch(() => {
|
} finally {
|
||||||
loadDBOptions();
|
if (!disposed && operateNode === currentNode.value) await loadDBOptions();
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const installCli = async () => {
|
const installCli = async () => {
|
||||||
loading.value = true;
|
if (cliSubmitting.value) return;
|
||||||
await installRedisCli()
|
cliSubmitting.value = true;
|
||||||
.then(() => {
|
try {
|
||||||
loading.value = false;
|
const res = await installRedisCli(newUUID(), operateNode);
|
||||||
redisCliExist.value = true;
|
if (disposed || operateNode !== currentNode.value) return;
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
if (res.data) {
|
||||||
reOpenTerminal();
|
cliTaskLogRef.value?.openWithTaskID(res.data, true, operateNode);
|
||||||
})
|
} else {
|
||||||
.catch(() => {
|
await checkCliValid();
|
||||||
loading.value = false;
|
}
|
||||||
});
|
} finally {
|
||||||
|
cliSubmitting.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadQuickCmd = async () => {
|
const loadQuickCmd = async () => {
|
||||||
@@ -403,6 +427,7 @@ const onAfter = () => {
|
|||||||
initTerminal();
|
initTerminal();
|
||||||
};
|
};
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
disposed = true;
|
||||||
closeTerminal(false);
|
closeTerminal(false);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user