1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: PHP 运行环境增加进程守护管理 (#6580)

This commit is contained in:
zhengkunwang 2024-09-25 21:58:55 +08:00 committed by GitHub
parent 4720bda29b
commit 7add6ab190
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 879 additions and 63 deletions

View File

@ -431,3 +431,67 @@ func (b *BaseApi) GetFPMConfig(c *gin.Context) {
}
helper.SuccessWithData(c, data)
}
// @Tags Runtime
// @Summary Get supervisor process
// @Description 获取 supervisor 进程
// @Accept json
// @Param id path integer true "request"
// @Success 200 {object} response.SupervisorProcess
// @Security ApiKeyAuth
// @Router /runtimes/supervisor/process/:id [get]
func (b *BaseApi) GetSupervisorProcess(c *gin.Context) {
id, err := helper.GetParamID(c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil)
return
}
data, err := runtimeService.GetSupervisorProcess(id)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Runtime
// @Summary Operate supervisor process
// @Description 操作 supervisor 进程
// @Accept json
// @Param request body request.PHPSupervisorProcessConfig true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/supervisor/process/operate [post]
func (b *BaseApi) OperateSupervisorProcess(c *gin.Context) {
var req request.PHPSupervisorProcessConfig
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := runtimeService.OperateSupervisorProcess(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Runtime
// @Summary Operate supervisor process file
// @Description 操作 supervisor 进程文件
// @Accept json
// @Param request body request.PHPSupervisorProcessFileReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/supervisor/process/file/operate [post]
func (b *BaseApi) OperateSupervisorProcessFile(c *gin.Context) {
var req request.PHPSupervisorProcessFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := runtimeService.OperateSupervisorProcessFile(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

View File

@ -33,6 +33,7 @@ type SupervisorProcessConfig struct {
Dir string `json:"dir"`
Numprocs string `json:"numprocs"`
}
type SupervisorProcessFileReq struct {
Name string `json:"name" validate:"required"`
Operate string `json:"operate" validate:"required,oneof=get clear update" `

View File

@ -100,3 +100,13 @@ type FPMConfig struct {
ID uint `json:"id" validate:"required"`
Params map[string]interface{} `json:"params" validate:"required"`
}
type PHPSupervisorProcessConfig struct {
ID uint `json:"id" validate:"required"`
SupervisorProcessConfig
}
type PHPSupervisorProcessFileReq struct {
ID uint `json:"id" validate:"required"`
SupervisorProcessFileReq
}

View File

@ -3,11 +3,13 @@ package service
import (
"bytes"
"fmt"
"os"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
@ -281,11 +283,6 @@ func (h *HostToolService) GetToolLog(req request.HostToolLogReq) (string, error)
func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcessConfig) error {
var (
supervisordDir = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord")
logDir = path.Join(supervisordDir, "log")
includeDir = path.Join(supervisordDir, "supervisor.d")
outLog = path.Join(logDir, fmt.Sprintf("%s.out.log", req.Name))
errLog = path.Join(logDir, fmt.Sprintf("%s.err.log", req.Name))
iniPath = path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name))
fileOp = files.NewFileOp()
)
if req.Operate == "update" || req.Operate == "create" {
@ -297,7 +294,22 @@ func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcess
return buserr.WithMap("ErrUserFindErr", map[string]interface{}{"name": req.User, "err": err.Error()}, err)
}
}
return handleProcess(supervisordDir, req, "")
}
func handleProcess(supervisordDir string, req request.SupervisorProcessConfig, containerName string) error {
var (
fileOp = files.NewFileOp()
logDir = path.Join(supervisordDir, "log")
includeDir = path.Join(supervisordDir, "supervisor.d")
outLog = path.Join(logDir, fmt.Sprintf("%s.out.log", req.Name))
errLog = path.Join(logDir, fmt.Sprintf("%s.err.log", req.Name))
iniPath = path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name))
)
if containerName != "" {
outLog = path.Join("/var/log/supervisor", fmt.Sprintf("%s.out.log", req.Name))
errLog = path.Join("/var/log/supervisor", fmt.Sprintf("%s.err.log", req.Name))
}
switch req.Operate {
case "create":
if fileOp.Stat(iniPath) {
@ -324,10 +336,10 @@ func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcess
if err = configFile.SaveTo(iniPath); err != nil {
return err
}
if err := operateSupervisorCtl("reread", "", ""); err != nil {
if err := operateSupervisorCtl("reread", "", "", includeDir, containerName); err != nil {
return err
}
return operateSupervisorCtl("update", "", "")
return operateSupervisorCtl("update", "", "", includeDir, containerName)
case "update":
configFile, err := ini.Load(iniPath)
if err != nil {
@ -350,52 +362,54 @@ func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcess
if err = configFile.SaveTo(iniPath); err != nil {
return err
}
if err := operateSupervisorCtl("reread", "", ""); err != nil {
if err := operateSupervisorCtl("reread", "", "", includeDir, containerName); err != nil {
return err
}
return operateSupervisorCtl("update", "", "")
return operateSupervisorCtl("update", "", "", includeDir, containerName)
case "restart":
return operateSupervisorCtl("restart", req.Name, "")
return operateSupervisorCtl("restart", req.Name, "", includeDir, containerName)
case "start":
return operateSupervisorCtl("start", req.Name, "")
return operateSupervisorCtl("start", req.Name, "", includeDir, containerName)
case "stop":
return operateSupervisorCtl("stop", req.Name, "")
return operateSupervisorCtl("stop", req.Name, "", includeDir, containerName)
case "delete":
_ = operateSupervisorCtl("remove", "", req.Name)
_ = files.NewFileOp().DeleteFile(iniPath)
_ = files.NewFileOp().DeleteFile(outLog)
_ = files.NewFileOp().DeleteFile(errLog)
if err := operateSupervisorCtl("reread", "", ""); err != nil {
_ = operateSupervisorCtl("remove", "", req.Name, includeDir, containerName)
_ = fileOp.DeleteFile(iniPath)
_ = fileOp.DeleteFile(outLog)
_ = fileOp.DeleteFile(errLog)
if err := operateSupervisorCtl("reread", "", "", includeDir, containerName); err != nil {
return err
}
return operateSupervisorCtl("update", "", "")
return operateSupervisorCtl("update", "", "", includeDir, containerName)
}
return nil
}
func (h *HostToolService) GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error) {
func handleProcessConfig(configDir, containerName string) ([]response.SupervisorProcessConfig, error) {
var (
result []response.SupervisorProcessConfig
)
configDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d")
fileList, _ := NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: configDir, Expand: true, Page: 1, PageSize: 100}})
if len(fileList.Items) == 0 {
return result, nil
entries, err := os.ReadDir(configDir)
if err != nil {
return nil, err
}
for _, configFile := range fileList.Items {
f, err := ini.Load(configFile.Path)
if err != nil {
global.LOG.Errorf("get %s file err %s", configFile.Name, err.Error())
for _, entry := range entries {
if entry.IsDir() {
continue
}
if strings.HasSuffix(configFile.Name, ".ini") {
fileName := entry.Name()
f, err := ini.Load(path.Join(configDir, fileName))
if err != nil {
global.LOG.Errorf("get %s file err %s", fileName, err.Error())
continue
}
if strings.HasSuffix(fileName, ".ini") {
config := response.SupervisorProcessConfig{}
name := strings.TrimSuffix(configFile.Name, ".ini")
name := strings.TrimSuffix(fileName, ".ini")
config.Name = name
section, err := f.GetSection(fmt.Sprintf("program:%s", name))
if err != nil {
global.LOG.Errorf("get %s file section err %s", configFile.Name, err.Error())
global.LOG.Errorf("get %s file section err %s", fileName, err.Error())
continue
}
if command, _ := section.GetKey("command"); command != nil {
@ -410,52 +424,69 @@ func (h *HostToolService) GetSupervisorProcessConfig() ([]response.SupervisorPro
if numprocs, _ := section.GetKey("numprocs"); numprocs != nil {
config.Numprocs = numprocs.Value()
}
_ = getProcessStatus(&config)
_ = getProcessStatus(&config, containerName)
result = append(result, config)
}
}
return result, nil
}
func (h *HostToolService) GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error) {
configDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d")
return handleProcessConfig(configDir, "")
}
func (h *HostToolService) OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (string, error) {
var (
includeDir = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d")
)
return handleSupervisorFile(req, includeDir, "", "")
}
func handleSupervisorFile(req request.SupervisorProcessFileReq, includeDir, containerName, logFile string) (string, error) {
var (
fileOp = files.NewFileOp()
group = fmt.Sprintf("program:%s", req.Name)
configPath = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d", fmt.Sprintf("%s.ini", req.Name))
configPath = path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name))
err error
)
switch req.File {
case "err.log":
logPath, err := ini_conf.GetIniValue(configPath, group, "stderr_logfile")
if err != nil {
return "", err
if logFile == "" {
logFile, err = ini_conf.GetIniValue(configPath, group, "stderr_logfile")
if err != nil {
return "", err
}
}
switch req.Operate {
case "get":
content, err := fileOp.GetContent(logPath)
content, err := fileOp.GetContent(logFile)
if err != nil {
return "", err
}
return string(content), nil
case "clear":
if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil {
if err = fileOp.WriteFile(logFile, strings.NewReader(""), 0755); err != nil {
return "", err
}
}
case "out.log":
logPath, err := ini_conf.GetIniValue(configPath, group, "stdout_logfile")
if err != nil {
return "", err
if logFile == "" {
logFile, err = ini_conf.GetIniValue(configPath, group, "stdout_logfile")
if err != nil {
return "", err
}
}
switch req.Operate {
case "get":
content, err := fileOp.GetContent(logPath)
content, err := fileOp.GetContent(logFile)
if err != nil {
return "", err
}
return string(content), nil
case "clear":
if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil {
if err = fileOp.WriteFile(logFile, strings.NewReader(""), 0755); err != nil {
return "", err
}
}
@ -475,17 +506,16 @@ func (h *HostToolService) OperateSupervisorProcessFile(req request.SupervisorPro
if err := fileOp.WriteFile(configPath, strings.NewReader(req.Content), 0755); err != nil {
return "", err
}
return "", operateSupervisorCtl("update", "", req.Name)
return "", operateSupervisorCtl("update", "", req.Name, includeDir, containerName)
}
}
return "", nil
}
func operateSupervisorCtl(operate, name, group string) error {
func operateSupervisorCtl(operate, name, group, includeDir, containerName string) error {
processNames := []string{operate}
if name != "" {
includeDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d")
f, err := ini.Load(path.Join(includeDir, fmt.Sprintf("%s.ini", name)))
if err != nil {
return err
@ -507,14 +537,21 @@ func operateSupervisorCtl(operate, name, group string) error {
processNames = append(processNames, group)
}
output, err := exec.Command("supervisorctl", processNames...).Output()
if err != nil {
if output != nil {
return errors.New(string(output))
}
return err
var (
output string
err error
)
if containerName != "" {
output, err = cmd.ExecWithTimeOut(fmt.Sprintf("docker exec %s supervisorctl %s", containerName, strings.Join(processNames, " ")), 2*time.Second)
} else {
var out []byte
out, err = exec.Command("supervisorctl", processNames...).Output()
output = string(out)
}
return nil
if err != nil && output != "" {
return errors.New(output)
}
return err
}
func getProcessName(name, numprocs string) []string {
@ -539,14 +576,27 @@ func getProcessName(name, numprocs string) []string {
return processNames
}
func getProcessStatus(config *response.SupervisorProcessConfig) error {
func getProcessStatus(config *response.SupervisorProcessConfig, containerName string) error {
var (
processNames = []string{"status"}
output string
err error
)
processNames = append(processNames, getProcessName(config.Name, config.Numprocs)...)
output, _ := exec.Command("supervisorctl", processNames...).Output()
lines := strings.Split(string(output), "\n")
if containerName != "" {
execStr := fmt.Sprintf("docker exec %s supervisorctl %s", containerName, strings.Join(processNames, " "))
output, err = cmd.ExecWithTimeOut(execStr, 3*time.Second)
} else {
var out []byte
out, err = exec.Command("supervisorctl", processNames...).Output()
output = string(out)
}
if containerName == "" && err != nil {
return err
}
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimPrefix(line, "stdout:")
fields := strings.Fields(line)
if len(fields) >= 5 {
status := response.ProcessStatus{

View File

@ -60,6 +60,10 @@ type IRuntimeService interface {
GetPHPConfigFile(req request.PHPFileReq) (*response.FileInfo, error)
UpdateFPMConfig(req request.FPMConfig) error
GetFPMConfig(id uint) (*request.FPMConfig, error)
GetSupervisorProcess(id uint) ([]response.SupervisorProcessConfig, error)
OperateSupervisorProcess(req request.PHPSupervisorProcessConfig) error
OperateSupervisorProcessFile(req request.PHPSupervisorProcessFileReq) (string, error)
}
func NewRuntimeService() IRuntimeService {
@ -994,3 +998,32 @@ func (r *RuntimeService) GetFPMConfig(id uint) (*request.FPMConfig, error) {
res := &request.FPMConfig{Params: params}
return res, nil
}
func (r *RuntimeService) GetSupervisorProcess(id uint) ([]response.SupervisorProcessConfig, error) {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return nil, err
}
configDir := path.Join(constant.RuntimeDir, "php", runtime.Name, "supervisor", "supervisor.d")
return handleProcessConfig(configDir, runtime.ContainerName)
}
func (r *RuntimeService) OperateSupervisorProcess(req request.PHPSupervisorProcessConfig) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
configDir := path.Join(constant.RuntimeDir, "php", runtime.Name, "supervisor")
return handleProcess(configDir, req.SupervisorProcessConfig, runtime.ContainerName)
}
func (r *RuntimeService) OperateSupervisorProcessFile(req request.PHPSupervisorProcessFileReq) (string, error) {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return "", err
}
supervisorDir := path.Join(constant.RuntimeDir, "php", runtime.Name, "supervisor")
configDir := path.Join(supervisorDir, "supervisor.d")
logFile := path.Join(supervisorDir, "log", fmt.Sprintf("%s.out.log", req.SupervisorProcessFileReq.Name))
return handleSupervisorFile(req.SupervisorProcessFileReq, configDir, runtime.ContainerName, logFile)
}

View File

@ -1344,6 +1344,7 @@ func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) error
server := servers[0]
if req.RuntimeID > 0 {
server.RemoveDirective("location", []string{"~", "[^/]\\.php(/|$)"})
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.RuntimeID))
if err != nil {
return err
@ -1462,11 +1463,7 @@ func (w WebsiteService) UpdateSitePermission(req request.WebsiteUpdateDirPermiss
if err != nil {
return err
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index")
absoluteIndexPath := GetSitePath(website, SiteIndexDir)
chownCmd := fmt.Sprintf("chown -R %s:%s %s", req.User, req.Group, absoluteIndexPath)
if cmd.HasNoPasswordSudo() {
chownCmd = fmt.Sprintf("sudo %s", chownCmd)

View File

@ -41,5 +41,9 @@ func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.POST("/php/file", baseApi.GetPHPConfigFile)
groupRouter.POST("/php/fpm/config", baseApi.UpdateFPMConfig)
groupRouter.GET("/php/fpm/config/:id", baseApi.GetFPMConfig)
groupRouter.GET("/supervisor/process/:id", baseApi.GetSupervisorProcess)
groupRouter.POST("/supervisor/process", baseApi.OperateSupervisorProcess)
groupRouter.POST("/supervisor/process/file", baseApi.OperateSupervisorProcessFile)
}
}

View File

@ -171,4 +171,28 @@ export namespace Runtime {
id: number;
params: any;
}
export interface ProcessReq {
operate: string;
name: string;
id: number;
}
export interface ProcessFileReq {
operate: string;
name: string;
content?: string;
file: string;
id: number;
}
export interface SupersivorProcess {
operate: string;
name: string;
command: string;
user: string;
dir: string;
numprocs: string;
id: number;
}
}

View File

@ -4,6 +4,7 @@ import { Runtime } from '../interface/runtime';
import { TimeoutEnum } from '@/enums/http-enum';
import { App } from '@/api/interface/app';
import { File } from '../interface/file';
import { HostTool } from '../interface/host-tool';
export const SearchRuntimes = (req: Runtime.RuntimeReq) => {
return http.post<ResPage<Runtime.RuntimeDTO>>(`/runtimes/search`, req);
@ -104,3 +105,19 @@ export const UpdateFPMConfig = (req: Runtime.FPMConfig) => {
export const GetFPMConfig = (id: number) => {
return http.get<Runtime.FPMConfig>(`/runtimes/php/fpm/config/${id}`);
};
export const GetSupervisorProcess = (id: number) => {
return http.get<HostTool.ProcessStatus[]>(`/runtimes/supervisor/process/${id}`);
};
export const OperateSupervisorProcess = (req: Runtime.ProcessReq) => {
return http.post(`/runtimes/supervisor/process`, req, TimeoutEnum.T_60S);
};
export const OperateSupervisorProcessFile = (req: Runtime.ProcessFileReq) => {
return http.post<string>(`/runtimes/supervisor/process/file`, req, TimeoutEnum.T_60S);
};
export const CreateSupervisorProcess = (req: Runtime.SupersivorProcess) => {
return http.post(`/runtimes/supervisor/process`, req);
};

View File

@ -85,6 +85,8 @@ const size = computed(() => {
return '50%';
case 'full':
return '100%';
case '60%':
return '60%';
default:
return '50%';
}

View File

@ -112,6 +112,7 @@
<ExtManagement ref="extManagementRef" @close="search" />
<ComposeLogs ref="composeLogRef" />
<Config ref="configRef" />
<Supervisor ref="supervisorRef" />
</div>
</template>
@ -133,6 +134,7 @@ import RouterMenu from '../index.vue';
import Log from '@/components/log-dialog/index.vue';
import ComposeLogs from '@/components/compose-log/index.vue';
import Config from '@/views/website/runtime/php/config/index.vue';
import Supervisor from '@/views/website/runtime/php/supervisor/index.vue';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
@ -157,6 +159,7 @@ const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const composeLogRef = ref();
const configRef = ref();
const supervisorRef = ref();
const buttons = [
{
@ -218,6 +221,15 @@ const buttons = [
return row.status === 'building';
},
},
{
label: i18n.global.t('menu.supervisor'),
click: function (row: Runtime.Runtime) {
openSupervisor(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'building';
},
},
{
label: i18n.global.t('commons.button.delete'),
disabled: function (row: Runtime.Runtime) {
@ -255,6 +267,10 @@ const openConfig = (row: Runtime.Runtime) => {
configRef.value.acceptParams(row);
};
const openSupervisor = (row: Runtime.Runtime) => {
supervisorRef.value.acceptParams(row.id);
};
const openLog = (row: Runtime.RuntimeDTO) => {
if (row.status == 'running') {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
@ -344,7 +360,7 @@ onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
}, 10000 * 1);
});
onUnmounted(() => {

View File

@ -0,0 +1,133 @@
<template>
<DrawerPro
v-model="open"
:header="process.operate == 'create' ? $t('commons.button.create') : $t('commons.button.edit')"
:back="handleClose"
size="small"
>
<el-form
ref="processForm"
label-position="top"
:model="process"
label-width="100px"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="process.name" :disabled="process.operate == 'update'"></el-input>
</el-form-item>
<el-form-item :label="$t('tool.supervisor.user')" prop="user">
<el-input v-model.trim="process.user"></el-input>
</el-form-item>
<el-form-item :label="$t('tool.supervisor.dir')" prop="dir">
<el-input v-model.trim="process.dir">
<template #prepend><FileList @choose="getPath" :dir="true"></FileList></template>
</el-input>
</el-form-item>
<el-form-item :label="$t('tool.supervisor.command')" prop="command">
<el-input v-model="process.command"></el-input>
</el-form-item>
<el-form-item :label="$t('tool.supervisor.numprocs')" prop="numprocsNum">
<el-input type="number" v-model.number="process.numprocsNum"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(processForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { CreateSupervisorProcess } from '@/api/modules/runtime';
import { Rules, checkNumberRange } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { HostTool } from '@/api/interface/host-tool';
const open = ref(false);
const loading = ref(false);
const processForm = ref<FormInstance>();
const rules = ref({
name: [Rules.requiredInput, Rules.supervisorName],
dir: [Rules.requiredInput],
command: [Rules.requiredInput],
user: [Rules.requiredInput],
numprocsNum: [Rules.requiredInput, Rules.integerNumber, checkNumberRange(1, 9999)],
});
const initData = (runtimeID: number) => ({
operate: 'create',
name: '',
command: '',
user: 'www-data',
dir: '',
numprocsNum: 1,
numprocs: '1',
id: runtimeID,
});
const process = ref(initData(0));
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
resetForm();
em('close', open);
};
const getPath = (path: string) => {
process.value.dir = path;
};
const resetForm = () => {
process.value = initData(0);
processForm.value?.resetFields();
};
const acceptParams = (operate: string, config: HostTool.SupersivorProcess, id: number) => {
process.value = initData(id);
if (operate == 'update') {
process.value = {
operate: 'update',
name: config.name,
command: config.command,
user: config.user,
dir: config.dir,
numprocsNum: 1,
numprocs: config.numprocs,
id: id,
};
process.value.numprocsNum = Number(config.numprocs);
}
open.value = true;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
process.value.numprocs = String(process.value.numprocsNum);
CreateSupervisorProcess(process.value)
.then(() => {
open.value = false;
em('close', open);
MsgSuccess(i18n.global.t('commons.msg.' + process.value.operate + 'Success'));
})
.finally(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,148 @@
<template>
<DrawerPro v-model="open" :header="title" :back="handleClose" size="large" :fullScreen="true">
<template #content>
<div v-if="req.file != 'config'">
<el-tabs v-model="req.file" type="card" @tab-click="handleChange">
<el-tab-pane :label="$t('logs.runLog')" name="out.log"></el-tab-pane>
<el-tab-pane :label="$t('logs.errLog')" name="err.log"></el-tab-pane>
</el-tabs>
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail">
{{ $t('commons.button.watch') }}
</el-checkbox>
<el-button class="ml-5" @click="cleanLog" icon="Delete">
{{ $t('commons.button.clean') }}
</el-button>
</div>
<br />
<div v-loading="loading">
<CodemirrorPro class="mt-5" v-model="content" :heightDiff="400"></CodemirrorPro>
</div>
</template>
<template #footer>
<span>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" :disabled="loading" @click="submit()" v-if="req.file === 'config'">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
<OpDialog ref="opRef" @search="getContent" />
</template>
<script lang="ts" setup>
import { onUnmounted, reactive, ref } from 'vue';
import { OperateSupervisorProcessFile } from '@/api/modules/runtime';
import i18n from '@/lang';
import { TabsPaneContext } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
const loading = ref(false);
const content = ref('');
const tailLog = ref(false);
const open = ref(false);
const req = reactive({
name: '',
file: 'conf',
operate: '',
content: '',
id: 0,
});
const title = ref('');
const opRef = ref();
let timer: NodeJS.Timer | null = null;
const em = defineEmits(['search']);
const getContent = () => {
loading.value = true;
OperateSupervisorProcessFile(req)
.then((res) => {
content.value = res.data;
})
.finally(() => {
loading.value = false;
});
};
const handleChange = (tab: TabsPaneContext) => {
req.file = tab.props.name.toString();
getContent();
};
const changeTail = () => {
if (tailLog.value) {
timer = setInterval(() => {
getContent();
}, 1000 * 5);
} else {
onCloseLog();
}
};
const handleClose = () => {
content.value = '';
open.value = false;
};
const submit = () => {
const updateReq = {
name: req.name,
operate: 'update',
file: req.file,
content: content.value,
id: req.id,
};
loading.value = true;
OperateSupervisorProcessFile(updateReq)
.then(() => {
em('search');
open.value = false;
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
})
.finally(() => {
loading.value = false;
});
};
const acceptParams = (name: string, file: string, operate: string, runtimeID: number) => {
req.name = name;
req.file = file;
req.operate = operate;
req.id = runtimeID;
title.value = file == 'config' ? i18n.global.t('website.source') : i18n.global.t('commons.button.log');
getContent();
open.value = true;
};
const cleanLog = async () => {
let log = req.file === 'out.log' ? i18n.global.t('logs.runLog') : i18n.global.t('logs.errLog');
opRef.value.acceptParams({
title: i18n.global.t('commons.msg.clean'),
names: [req.name],
msg: i18n.global.t('commons.msg.operatorHelper', [log, i18n.global.t('commons.msg.clean')]),
api: OperateSupervisorProcessFile,
params: { name: req.name, operate: 'clear', file: req.file },
});
};
const onCloseLog = async () => {
tailLog.value = false;
clearInterval(Number(timer));
timer = null;
};
onUnmounted(() => {
onCloseLog();
});
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.fullScreen {
border: none;
}
</style>

View File

@ -0,0 +1,316 @@
<template>
<DrawerPro v-model="open" :header="$t('tool.supervisor.list')" size="60%" :back="handleClose">
<template #content>
<ComplexTable :data="data" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('commons.button.create') + $t('tool.supervisor.list') }}
</el-button>
</template>
<el-table-column
:label="$t('commons.table.name')"
fix
prop="name"
min-width="80px"
show-overflow-tooltip
></el-table-column>
<el-table-column
:label="$t('tool.supervisor.command')"
prop="command"
min-width="100px"
fix
show-overflow-tooltip
></el-table-column>
<el-table-column
:label="$t('tool.supervisor.dir')"
prop="dir"
min-width="100px"
fix
show-overflow-tooltip
></el-table-column>
<el-table-column
:label="$t('tool.supervisor.user')"
prop="user"
show-overflow-tooltip
min-width="60px"
></el-table-column>
<el-table-column
:label="$t('tool.supervisor.numprocs')"
prop="numprocs"
min-width="60px"
></el-table-column>
<el-table-column :label="$t('tool.supervisor.manage')" min-width="80px">
<template #default="{ row }">
<div v-if="row.status && row.status.length > 0 && row.hasLoad">
<el-button
v-if="checkStatus(row.status) === 'RUNNING'"
link
type="success"
:icon="VideoPlay"
@click="operate('stop', row.name)"
>
{{ $t('commons.status.running') }}
</el-button>
<el-button
v-else-if="checkStatus(row.status) === 'WARNING'"
link
type="warning"
:icon="RefreshRight"
@click="operate('restart', row.name)"
>
{{ $t('commons.status.unhealthy') }}
</el-button>
<el-button v-else link type="danger" :icon="VideoPause" @click="operate('start', row.name)">
{{ $t('commons.status.stopped') }}
</el-button>
</div>
<div v-if="!row.hasLoad">
<el-button link loading></el-button>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" min-width="60px">
<template #default="{ row }">
<div v-if="row.hasLoad">
<el-popover placement="bottom" :width="600" trigger="hover">
<template #reference>
<el-button type="primary" link v-if="row.status.length > 1">
{{ $t('website.check') }}
</el-button>
<el-button type="primary" link v-else>
<span>{{ $t('tool.supervisor.' + row.status[0].status) }}</span>
</el-button>
</template>
<el-table :data="row.status">
<el-table-column
property="name"
:label="$t('commons.table.name')"
fix
show-overflow-tooltip
/>
<el-table-column
property="status"
:label="$t('tool.supervisor.statusCode')"
width="100px"
/>
<el-table-column property="PID" label="PID" width="100px" />
<el-table-column
property="uptime"
:label="$t('tool.supervisor.uptime')"
width="100px"
/>
<el-table-column
property="msg"
:label="$t('tool.supervisor.msg')"
fix
show-overflow-tooltip
/>
</el-table>
</el-popover>
</div>
<div v-if="!row.hasLoad">
<el-button link loading></el-button>
</div>
</template>
</el-table-column>
<fu-table-operations
:ellipsis="6"
:buttons="buttons"
:label="$t('commons.table.operate')"
:fixed="mobile ? false : 'right'"
width="280px"
fix
/>
</ComplexTable>
</template>
</DrawerPro>
<File ref="fileRef" @search="search" />
<Create ref="createRef" @close="search" />
</template>
<script setup lang="ts">
import { ref } from '@vue/runtime-core';
import { computed } from 'vue';
import Create from './create/index.vue';
import File from './file/index.vue';
import { GetSupervisorProcess, OperateSupervisorProcess } from '@/api/modules/runtime';
import { GlobalStore } from '@/store';
import i18n from '@/lang';
import { HostTool } from '@/api/interface/host-tool';
import { MsgSuccess } from '@/utils/message';
import { VideoPlay, VideoPause, RefreshRight } from '@element-plus/icons-vue';
const globalStore = GlobalStore();
const loading = ref(false);
const fileRef = ref();
const data = ref();
const createRef = ref();
const dataLoading = ref(false);
const open = ref(false);
const runtimeID = ref(0);
const handleClose = () => {
open.value = false;
};
const acceptParams = async (id: number) => {
runtimeID.value = id;
search();
open.value = true;
};
const openCreate = () => {
createRef.value.acceptParams('create', undefined, runtimeID.value);
};
const search = async () => {
let needLoadStatus = false;
dataLoading.value = true;
try {
const res = await GetSupervisorProcess(runtimeID.value);
data.value = res.data;
for (const process of data.value) {
if (process.status && process.status.length > 0) {
process.hasLoad = true;
} else {
process.hasLoad = false;
needLoadStatus = true;
}
}
if (needLoadStatus) {
setTimeout(loadStatus, 1000);
}
} catch (error) {
} finally {
dataLoading.value = false;
}
};
const loadStatus = async () => {
let needLoadStatus = false;
try {
const res = await GetSupervisorProcess(runtimeID.value);
const stats = res.data || [];
for (const process of data.value) {
for (const item of stats) {
if (process.name === item.name) {
if (item.status && item.status.length > 0) {
process.status = item.status;
process.hasLoad = true;
} else {
needLoadStatus = true;
}
}
}
}
if (needLoadStatus) {
setTimeout(loadStatus, 20000);
}
} catch (error) {}
};
const mobile = computed(() => {
return globalStore.isMobile();
});
const checkStatus = (status: HostTool.ProcessStatus[]): string => {
if (!status || status.length === 0) return 'STOPPED';
const statusCounts = status.reduce((acc, curr) => {
acc[curr.status] = (acc[curr.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
if (statusCounts['STARTING']) return 'STARTING';
if (statusCounts['RUNNING'] === status.length) return 'RUNNING';
if (statusCounts['RUNNING'] > 0) return 'WARNING';
return 'STOPPED';
};
const operate = async (operation: string, name: string) => {
try {
ElMessageBox.confirm(
i18n.global.t('tool.supervisor.operatorHelper', [name, i18n.global.t('app.' + operation)]),
i18n.global.t('app.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
)
.then(() => {
loading.value = true;
OperateSupervisorProcess({ operate: operation, name: name, id: runtimeID.value })
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {})
.finally(() => {
loading.value = false;
});
})
.catch(() => {});
} catch (error) {}
};
const getFile = (name: string, file: string, runtimeID: number) => {
fileRef.value.acceptParams(name, file, 'get', runtimeID);
};
const edit = (row: HostTool.SupersivorProcess) => {
createRef.value.acceptParams('update', row);
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: function (row: HostTool.SupersivorProcess) {
edit(row);
},
show: function (row: HostTool.SupersivorProcess) {
return row.name != 'php-fpm';
},
},
{
label: i18n.global.t('website.proxyFile'),
click: function (row: HostTool.SupersivorProcess) {
getFile(row.name, 'config', runtimeID.value);
},
show: function (row: HostTool.SupersivorProcess) {
return row.name != 'php-fpm';
},
},
{
label: i18n.global.t('website.log'),
click: function (row: HostTool.SupersivorProcess) {
getFile(row.name, 'out.log', runtimeID.value);
},
show: function (row: HostTool.SupersivorProcess) {
return row.name != 'php-fpm';
},
},
{
label: i18n.global.t('commons.button.restart'),
click: function (row: HostTool.SupersivorProcess) {
operate('restart', row.name);
},
show: function (row: HostTool.SupersivorProcess) {
return row.name != 'php-fpm';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: HostTool.SupersivorProcess) {
operate('delete', row.name);
},
show: function (row: HostTool.SupersivorProcess) {
return row.name != 'php-fpm';
},
},
];
defineExpose({
acceptParams,
});
</script>

View File

@ -43,7 +43,7 @@
:label="'PHP'"
v-if="(website.type === 'runtime' && website.runtimeType === 'php') || website.type === 'static'"
>
<PHP :website="website" v-if="tabIndex == '12'"></PHP>
<PHP :website="website" v-if="tabIndex == '13'"></PHP>
</el-tab-pane>
</el-tabs>
</template>

View File

@ -76,6 +76,7 @@ const submit = async (form: FormInstance) => {
taskID: taskID,
mirror: build.value.mirror,
});
handleClose();
openTaskLog(taskID);
} catch (error) {}
});