feat: 增加守护进程管理 (#1800)

pull/1802/head
zhengkunwang 2023-08-01 17:31:42 +08:00 committed by GitHub
parent b31de5e637
commit 39abd4341d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1035 additions and 47 deletions

View File

@ -172,3 +172,42 @@ func (b *BaseApi) OperateProcess(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags Host tool
// @Summary Get Supervisor process config
// @Description 获取 Supervisor 进程配置
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Router /host/tool/supervisor/process [get]
func (b *BaseApi) GetProcess(c *gin.Context) {
configs, err := hostToolService.GetSupervisorProcessConfig()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, configs)
}
// @Tags Host tool
// @Summary Get Supervisor process config
// @Description 操作 Supervisor 进程文件
// @Accept json
// @Param request body request.SupervisorProcessFileReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /host/tool/supervisor/process/file [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
func (b *BaseApi) GetProcessFile(c *gin.Context) {
var req request.SupervisorProcessFileReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
content, err := hostToolService.OperateSupervisorProcessFile(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, content)
}

View File

@ -85,7 +85,7 @@ func (b *BaseApi) ListRemoteDB(c *gin.Context) {
// @Tags Database
// @Summary Get remote databases
// @Description 获取远程数据库
// @Success 200 dto.RemoteDBOption
// @Success 200 {object} dto.RemoteDBInfo
// @Security ApiKeyAuth
// @Router /databases/remote/:name [get]
func (b *BaseApi) GetRemoteDB(c *gin.Context) {

View File

@ -32,3 +32,9 @@ 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" `
Content string `json:"content"`
File string `json:"file" validate:"required,oneof=out.log err.log config"`
}

View File

@ -20,3 +20,20 @@ type Supervisor struct {
type HostToolConfig struct {
Content string `json:"content"`
}
type SupervisorProcessConfig struct {
Name string `json:"name"`
Command string `json:"command"`
User string `json:"user"`
Dir string `json:"dir"`
Numprocs string `json:"numprocs"`
Msg string `json:"msg"`
Status []ProcessStatus `json:"status"`
}
type ProcessStatus struct {
Name string `json:"name"`
Status string `json:"status"`
PID string `json:"PID"`
Uptime string `json:"uptime"`
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@ -14,7 +15,9 @@ import (
"github.com/pkg/errors"
"gopkg.in/ini.v1"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
)
@ -27,6 +30,8 @@ type IHostToolService interface {
OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error)
GetToolLog(req request.HostToolLogReq) (string, error)
OperateSupervisorProcess(req request.SupervisorProcessConfig) error
GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error)
OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (string, error)
}
func NewIHostToolService() IHostToolService {
@ -99,7 +104,7 @@ func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.Host
configPath := "/etc/supervisord.conf"
if !fileOp.Stat(configPath) {
configPath = "/etc/supervisor/supervisord.conf"
if !fileOp.Stat("configPath") {
if !fileOp.Stat(configPath) {
return nil, errors.New("ErrConfigNotFound")
}
}
@ -180,7 +185,6 @@ func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*respon
configPath = pathSet.Value
}
}
configPath = "/etc/supervisord.conf"
switch req.Operate {
case "get":
content, err := fileOp.GetContent(configPath)
@ -233,27 +237,277 @@ func (h *HostToolService) GetToolLog(req request.HostToolLogReq) (string, error)
}
func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcessConfig) error {
configFile := ini.Empty()
supervisordDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord")
logDir := path.Join(supervisordDir, "log")
includeDir := path.Join(supervisordDir, "supervisor.d")
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 == "edit" || req.Operate == "create" {
if !fileOp.Stat(req.Dir) {
return buserr.New("ErrConfigDirNotFound")
}
_, err := user.Lookup(req.User)
if err != nil {
return buserr.WithMap("ErrUserFindErr", map[string]interface{}{"name": req.User, "err": err.Error()}, err)
}
}
section, err := configFile.NewSection(fmt.Sprintf("program:%s", req.Name))
switch req.Operate {
case "create":
if fileOp.Stat(iniPath) {
return buserr.New("ErrConfigAlreadyExist")
}
configFile := ini.Empty()
section, err := configFile.NewSection(fmt.Sprintf("program:%s", req.Name))
if err != nil {
return err
}
_, _ = section.NewKey("command", req.Command)
_, _ = section.NewKey("directory", req.Dir)
_, _ = section.NewKey("autorestart", "true")
_, _ = section.NewKey("startsecs", "3")
_, _ = section.NewKey("stdout_logfile", outLog)
_, _ = section.NewKey("stderr_logfile", errLog)
_, _ = section.NewKey("stdout_logfile_maxbytes", "2MB")
_, _ = section.NewKey("stderr_logfile_maxbytes", "2MB")
_, _ = section.NewKey("user", req.User)
_, _ = section.NewKey("priority", "999")
_, _ = section.NewKey("numprocs", req.Numprocs)
_, _ = section.NewKey("process_name", "%(program_name)s_%(process_num)02d")
if err = configFile.SaveTo(iniPath); err != nil {
return err
}
return operateSupervisorCtl("reload", "", "")
case "edit":
configFile, err := ini.Load(iniPath)
if err != nil {
return err
}
section, err := configFile.GetSection(fmt.Sprintf("program:%s", req.Name))
if err != nil {
return err
}
commandKey := section.Key("command")
commandKey.SetValue(req.Command)
directoryKey := section.Key("directory")
directoryKey.SetValue(req.Dir)
userKey := section.Key("user")
userKey.SetValue(req.User)
numprocsKey := section.Key("numprocs")
numprocsKey.SetValue(req.Numprocs)
if err = configFile.SaveTo(iniPath); err != nil {
return err
}
return operateSupervisorCtl("reload", "", "")
case "restart":
return operateSupervisorCtl("restart", req.Name, "")
case "start":
return operateSupervisorCtl("start", req.Name, "")
case "stop":
return operateSupervisorCtl("stop", req.Name, "")
case "delete":
_ = operateSupervisorCtl("remove", "", req.Name)
_ = files.NewFileOp().DeleteFile(iniPath)
_ = files.NewFileOp().DeleteFile(outLog)
_ = files.NewFileOp().DeleteFile(errLog)
_ = operateSupervisorCtl("reload", "", "")
}
return nil
}
func (h *HostToolService) GetSupervisorProcessConfig() ([]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
}
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())
continue
}
if strings.HasSuffix(configFile.Name, ".ini") {
config := response.SupervisorProcessConfig{}
name := strings.TrimSuffix(configFile.Name, ".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())
continue
}
if command, _ := section.GetKey("command"); command != nil {
config.Command = command.Value()
}
if directory, _ := section.GetKey("directory"); directory != nil {
config.Dir = directory.Value()
}
if user, _ := section.GetKey("user"); user != nil {
config.User = user.Value()
}
if numprocs, _ := section.GetKey("numprocs"); numprocs != nil {
config.Numprocs = numprocs.Value()
}
_ = getProcessStatus(&config)
result = append(result, config)
}
}
return result, nil
}
func (h *HostToolService) OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (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))
)
switch req.File {
case "err.log":
logPath, err := ini_conf.GetIniValue(configPath, group, "stderr_logfile")
if err != nil {
return "", err
}
switch req.Operate {
case "get":
content, err := fileOp.GetContent(logPath)
if err != nil {
return "", err
}
return string(content), nil
case "clear":
if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil {
return "", err
}
}
case "out.log":
logPath, err := ini_conf.GetIniValue(configPath, group, "stdout_logfile")
if err != nil {
return "", err
}
switch req.Operate {
case "get":
content, err := fileOp.GetContent(logPath)
if err != nil {
return "", err
}
return string(content), nil
case "clear":
if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil {
return "", err
}
}
case "config":
switch req.Operate {
case "get":
content, err := fileOp.GetContent(configPath)
if err != nil {
return "", err
}
return string(content), nil
case "update":
if req.Content == "" {
return "", buserr.New("ErrConfigIsNull")
}
if err := fileOp.WriteFile(configPath, strings.NewReader(req.Content), 0755); err != nil {
return "", err
}
return "", operateSupervisorCtl("update", "", req.Name)
}
}
return "", nil
}
func operateSupervisorCtl(operate, name, group 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
}
section, err := f.GetSection(fmt.Sprintf("program:%s", name))
if err != nil {
return err
}
numprocsNum := ""
if numprocs, _ := section.GetKey("numprocs"); numprocs != nil {
numprocsNum = numprocs.Value()
}
if numprocsNum == "" {
return buserr.New("ErrConfigParse")
}
processNames = append(processNames, getProcessName(name, numprocsNum)...)
}
if group != "" {
processNames = append(processNames, group)
}
output, err := exec.Command("supervisorctl", processNames...).Output()
if err != nil {
if output != nil {
return errors.New(string(output))
}
return err
}
_, _ = section.NewKey("command", req.Command)
_, _ = section.NewKey("directory", req.Dir)
_, _ = section.NewKey("autorestart", "true")
_, _ = section.NewKey("startsecs", "3")
_, _ = section.NewKey("stdout_logfile", path.Join(logDir, fmt.Sprintf("%s.out.log", req.Name)))
_, _ = section.NewKey("stderr_logfile", path.Join(logDir, fmt.Sprintf("%s.err.log", req.Name)))
_, _ = section.NewKey("stdout_logfile_maxbytes", "2MB")
_, _ = section.NewKey("stderr_logfile_maxbytes", "2MB")
_, _ = section.NewKey("user", req.User)
_, _ = section.NewKey("priority", "999")
_, _ = section.NewKey("numprocs", req.Numprocs)
_, _ = section.NewKey("process_name", "%(program_name)s_%(process_num)02d")
return configFile.SaveTo(path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name)))
return nil
}
func getProcessName(name, numprocs string) []string {
var (
processNames []string
)
num, err := strconv.Atoi(numprocs)
if err != nil {
return processNames
}
if num == 1 {
processNames = append(processNames, fmt.Sprintf("%s:%s_00", name, name))
} else {
for i := 0; i < num; i++ {
processName := fmt.Sprintf("%s:%s_0%s", name, name, strconv.Itoa(i))
if i >= 10 {
processName = fmt.Sprintf("%s:%s_%s", name, name, strconv.Itoa(i))
}
processNames = append(processNames, processName)
}
}
return processNames
}
func getProcessStatus(config *response.SupervisorProcessConfig) error {
var (
processNames = []string{"status"}
)
processNames = append(processNames, getProcessName(config.Name, config.Numprocs)...)
output, _ := exec.Command("supervisorctl", processNames...).Output()
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 5 {
status := response.ProcessStatus{
Name: fields[0],
Status: fields[1],
}
if fields[1] == "RUNNING" {
status.PID = strings.TrimSuffix(fields[3], ",")
status.Uptime = fields[5]
}
config.Status = append(config.Status, status)
}
}
return nil
}

View File

@ -100,4 +100,9 @@ ErrBackupInUsed: "The backup account is already being used in a cronjob and cann
ErrOSSConn: "Unable to successfully request the latest version. Please check if the server can connect to the external network environment."
#tool
ErrConfigNotFound: "Configuration file does not exist"
ErrConfigNotFound: "Configuration file does not exist"
ErrConfigParse: "Configuration file format error"
ErrConfigIsNull: "The configuration file is not allowed to be empty"
ErrConfigDirNotFound: "The running directory does not exist"
ErrConfigAlreadyExist: "A configuration file with the same name already exists"
ErrUserFindErr: "Failed to find user {{ .name }} {{ .err }}"

View File

@ -101,3 +101,8 @@ ErrOSSConn: "無法成功請求最新版本,請檢查伺服器是否能夠連
#tool
ErrConfigNotFound: "配置文件不存在"
ErrConfigParse: "配置文件格式有誤"
ErrConfigIsNull: "配置文件不允許為空"
ErrConfigDirNotFound: "運行目錄不存在"
ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}"

View File

@ -101,3 +101,8 @@ ErrOSSConn: "无法成功请求最新版本,请检查服务器是否能够连
#tool
ErrConfigNotFound: "配置文件不存在"
ErrConfigParse: "配置文件格式有误"
ErrConfigIsNull: "配置文件不允许为空"
ErrConfigDirNotFound: "运行目录不存在"
ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}"

View File

@ -54,5 +54,7 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) {
hostRouter.POST("/tool/config", baseApi.OperateToolConfig)
hostRouter.POST("/tool/log", baseApi.GetToolLog)
hostRouter.POST("/tool/supervisor/process", baseApi.OperateProcess)
hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess)
hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile)
}
}

View File

@ -6154,6 +6154,26 @@ const docTemplate = `{
}
},
"/host/tool/supervisor/process": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Supervisor 进程配置",
"consumes": [
"application/json"
],
"tags": [
"Host tool"
],
"summary": "Get Supervisor process config",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"security": [
{
@ -6195,6 +6215,48 @@ const docTemplate = `{
}
}
},
"/host/tool/supervisor/process/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "操作 Supervisor 进程文件",
"consumes": [
"application/json"
],
"tags": [
"Host tool"
],
"summary": "Get Supervisor process config",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.SupervisorProcessFileReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"operate"
],
"formatEN": "[operate] [operate] Supervisor 进程文件",
"formatZH": "[operate] Supervisor 进程文件 ",
"paramKeys": []
}
}
},
"/hosts": {
"post": {
"security": [
@ -15696,6 +15758,37 @@ const docTemplate = `{
}
}
},
"request.SupervisorProcessFileReq": {
"type": "object",
"required": [
"file",
"name",
"operate"
],
"properties": {
"content": {
"type": "string"
},
"file": {
"type": "string",
"enum": [
"out.log",
"err.log"
]
},
"name": {
"type": "string"
},
"operate": {
"type": "string",
"enum": [
"get",
"clear",
"update"
]
}
}
},
"request.WebsiteAcmeAccountCreate": {
"type": "object",
"required": [

View File

@ -6147,6 +6147,26 @@
}
},
"/host/tool/supervisor/process": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Supervisor 进程配置",
"consumes": [
"application/json"
],
"tags": [
"Host tool"
],
"summary": "Get Supervisor process config",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"security": [
{
@ -6188,6 +6208,48 @@
}
}
},
"/host/tool/supervisor/process/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "操作 Supervisor 进程文件",
"consumes": [
"application/json"
],
"tags": [
"Host tool"
],
"summary": "Get Supervisor process config",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.SupervisorProcessFileReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"operate"
],
"formatEN": "[operate] [operate] Supervisor 进程文件",
"formatZH": "[operate] Supervisor 进程文件 ",
"paramKeys": []
}
}
},
"/hosts": {
"post": {
"security": [
@ -15689,6 +15751,37 @@
}
}
},
"request.SupervisorProcessFileReq": {
"type": "object",
"required": [
"file",
"name",
"operate"
],
"properties": {
"content": {
"type": "string"
},
"file": {
"type": "string",
"enum": [
"out.log",
"err.log"
]
},
"name": {
"type": "string"
},
"operate": {
"type": "string",
"enum": [
"get",
"clear",
"update"
]
}
}
},
"request.WebsiteAcmeAccountCreate": {
"type": "object",
"required": [

View File

@ -2978,6 +2978,28 @@ definitions:
user:
type: string
type: object
request.SupervisorProcessFileReq:
properties:
content:
type: string
file:
enum:
- out.log
- err.log
type: string
name:
type: string
operate:
enum:
- get
- clear
- update
type: string
required:
- file
- name
- operate
type: object
request.WebsiteAcmeAccountCreate:
properties:
email:
@ -7730,6 +7752,18 @@ paths:
formatZH: '[operate] [type] '
paramKeys: []
/host/tool/supervisor/process:
get:
consumes:
- application/json
description: 获取 Supervisor 进程配置
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
@ -7756,6 +7790,33 @@ paths:
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/host/tool/supervisor/process/file:
post:
consumes:
- application/json
description: 操作 Supervisor 进程文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFuntions: []
bodyKeys:
- operate
formatEN: '[operate] [operate] Supervisor 进程文件'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts:
post:
consumes:

View File

@ -39,5 +39,25 @@ export namespace HostTool {
user: string;
dir: string;
numprocs: string;
status?: ProcessStatus[];
}
export interface ProcessStatus {
PID: string;
status: string;
uptime: string;
name: string;
}
export interface ProcessReq {
operate: string;
name: string;
}
export interface ProcessFileReq {
operate: string;
name: string;
content?: string;
file: string;
}
}

View File

@ -21,6 +21,18 @@ export const InitSupervisor = (req: HostTool.SupersivorInit) => {
return http.post<any>(`/hosts/tool/init`, req);
};
export const OperateSupervisorProcess = (req: HostTool.SupersivorProcess) => {
export const CreateSupervisorProcess = (req: HostTool.SupersivorProcess) => {
return http.post<any>(`/hosts/tool/supervisor/process`, req);
};
export const OperateSupervisorProcess = (req: HostTool.ProcessReq) => {
return http.post<any>(`/hosts/tool/supervisor/process`, req, 100000);
};
export const GetSupervisorProcess = () => {
return http.get<HostTool.SupersivorProcess>(`/hosts/tool/supervisor/process`);
};
export const OperateSupervisorProcessFile = (req: HostTool.ProcessFileReq) => {
return http.post<any>(`/hosts/tool/supervisor/process/file`, req, 100000);
};

View File

@ -1225,6 +1225,7 @@ const message = {
appHelper: 'Please view the installation instructions of some applications on the application details page',
backupApp: 'Backup application before upgrade',
backupAppHelper: 'If the upgrade fails, you can use the application backup to roll back',
delete: 'Delete',
},
website: {
website: 'Website',
@ -1665,7 +1666,8 @@ const message = {
numprocs: 'Number of processes',
initWarn:
'Because it is not compatible with the original configuration, initializing Supervisor will modify the files parameter of the configuration file, causing all existing processes to stop, please confirm the risk in advance. The modified process configuration folder is in <1Panel installation directory>/1panel/tools/supervisord/supervisor.d',
operatorHelper: 'Operation {0} will be performed on Supervisor, continue? ',
operatorHelper: 'Operation {1} will be performed on {0}, continue? ',
uptime: 'running time',
},
},
};

View File

@ -1170,6 +1170,7 @@ const message = {
appHelper: '使',
backupApp: '',
backupAppHelper: '使',
delete: '',
},
website: {
website: '',
@ -1582,7 +1583,8 @@ const message = {
numprocs: '',
initWarn:
' Supervisor files <1Panel>/1panel/tools/supervisord/supervisor.d ',
operatorHelper: ' Supervisor {0} ',
operatorHelper: ' {0} {1} ',
uptime: '',
},
},
};

View File

@ -1170,6 +1170,7 @@ const message = {
appHelper: '使',
backupApp: '',
backupAppHelper: '使',
delete: '',
},
website: {
website: '',
@ -1584,7 +1585,8 @@ const message = {
numprocs: '',
initWarn:
' Supervisor files <1Panel>/1panel/tools/supervisord/supervisor.d ',
operatorHelper: ' Supervisor {0} ',
operatorHelper: ' {0} {1} ',
uptime: '',
},
},
};

View File

@ -1,13 +1,16 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="30%">
<template #header>
<DrawerHeader :header="$t('commons.button.create')" :back="handleClose" />
<DrawerHeader
:header="process.operate == 'create' ? $t('commons.button.create') : $t('commons.button.edit')"
:back="handleClose"
/>
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form ref="processForm" label-position="top" :model="process" label-width="100px" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="process.name"></el-input>
<el-input v-model.trim="process.name" :disabled="process.operate == 'edit'"></el-input>
</el-form-item>
<el-form-item :label="$t('tool.supervisor.user')" prop="user">
<el-input v-model.trim="process.user"></el-input>
@ -20,8 +23,8 @@
<el-form-item :label="$t('tool.supervisor.command')" prop="command">
<el-input v-model.trim="process.command"></el-input>
</el-form-item>
<el-form-item :label="$t('tool.supervisor.numprocs')" prop="numprocs">
<el-input v-model.trim="process.numprocs"></el-input>
<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>
</el-col>
@ -38,13 +41,14 @@
</template>
<script lang="ts" setup>
import { OperateSupervisorProcess } from '@/api/modules/host-tool';
import { Rules } from '@/global/form-rules';
import { CreateSupervisorProcess } from '@/api/modules/host-tool';
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);
@ -54,16 +58,18 @@ const rules = ref({
dir: [Rules.requiredInput],
command: [Rules.requiredInput],
user: [Rules.requiredInput],
numprocs: [Rules.requiredInput],
numprocsNum: [Rules.requiredInput, checkNumberRange(1, 9999)],
});
const process = ref({
const initData = () => ({
operate: 'create',
name: '',
command: '',
user: '',
dir: '',
numprocsNum: 1,
numprocs: '1',
});
const process = ref(initData());
const em = defineEmits(['close']);
const handleClose = () => {
@ -77,10 +83,24 @@ const getPath = (path: string) => {
};
const resetForm = () => {
process.value = initData();
processForm.value?.resetFields();
};
const acceptParams = () => {
const acceptParams = (operate: string, config: HostTool.SupersivorProcess) => {
process.value = initData();
if (operate == 'edit') {
process.value = {
operate: 'edit',
name: config.name,
command: config.command,
user: config.user,
dir: config.dir,
numprocsNum: 1,
numprocs: config.numprocs,
};
process.value.numprocsNum = Number(config.numprocs);
}
open.value = true;
};
@ -91,10 +111,12 @@ const submit = async (formEl: FormInstance | undefined) => {
return;
}
loading.value = true;
OperateSupervisorProcess(process.value)
process.value.numprocs = String(process.value.numprocsNum);
CreateSupervisorProcess(process.value)
.then(() => {
open.value = false;
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
em('close', open);
MsgSuccess(i18n.global.t('commons.msg.' + process.value.operate + 'Success'));
})
.finally(() => {
loading.value = false;

View File

@ -0,0 +1,164 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader :header="title" :back="handleClose"></DrawerHeader>
</template>
<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" style="float: left" @change="changeTail">
{{ $t('commons.button.watch') }}
</el-checkbox>
<el-button style="margin-left: 20px" @click="cleanLog" icon="Delete">
{{ $t('commons.button.clean') }}
</el-button>
</div>
<br />
<div v-loading="loading">
<codemirror
style="height: calc(100vh - 430px)"
:autofocus="true"
:placeholder="$t('website.noLog')"
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="content"
@ready="handleReady"
/>
</div>
<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>
</el-drawer>
</template>
<script lang="ts" setup>
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
import { useDeleteData } from '@/hooks/use-delete-data';
import { OperateSupervisorProcessFile } from '@/api/modules/host-tool';
import i18n from '@/lang';
import { TabsPaneContext } from 'element-plus';
const extensions = [javascript(), oneDark];
const loading = ref(false);
const content = ref('');
const tailLog = ref(false);
const open = ref(false);
const req = reactive({
name: '',
file: 'conf',
operate: '',
content: '',
});
const title = ref('');
const view = shallowRef();
const handleReady = (payload) => {
view.value = payload.view;
};
let timer: NodeJS.Timer | null = null;
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,
};
loading.value = true;
OperateSupervisorProcessFile(updateReq)
.then(() => {
getContent();
})
.finally(() => {
loading.value = false;
});
};
const acceptParams = (name: string, file: string, operate: string) => {
req.name = name;
req.file = file;
req.operate = operate;
title.value = file == 'config' ? i18n.global.t('website.source') : i18n.global.t('commons.button.log');
getContent();
open.value = true;
};
const cleanLog = async () => {
const clearReq = {
name: req.name,
operate: 'clear',
file: req.file,
};
try {
await useDeleteData(OperateSupervisorProcessFile, clearReq, 'commons.msg.delete');
getContent();
} catch (error) {
} finally {
}
};
const onCloseLog = async () => {
tailLog.value = false;
clearInterval(Number(timer));
timer = null;
};
onMounted(() => {
getContent();
});
onUnmounted(() => {
onCloseLog();
});
defineExpose({
acceptParams,
});
</script>

View File

@ -1,9 +1,18 @@
<template>
<div>
<ToolRouter />
<el-card v-if="!isRunningSuperVisor && maskShow" class="mask-prompt">
<span>{{ $t('firewall.firewallNotStart') }}</span>
</el-card>
<LayoutContent :title="$t('tool.supervisor.list')" v-loading="loading">
<template #app>
<SuperVisorStatus @setting="setting" v-model:loading="loading" @is-exist="isExist" />
<SuperVisorStatus
@setting="setting"
v-model:loading="loading"
@is-exist="isExist"
@is-running="isRunning"
v-model:mask-show="maskShow"
/>
</template>
<template v-if="isExistSuperVisor && !setSuperVisor" #toolbar>
<el-button type="primary" @click="openCreate">
@ -11,11 +20,52 @@
</el-button>
</template>
<template #main v-if="isExistSuperVisor && !setSuperVisor">
<ComplexTable></ComplexTable>
<ComplexTable :data="data" :class="{ mask: !isRunningSuperVisor }">
<el-table-column :label="$t('commons.table.name')" fix prop="name"></el-table-column>
<el-table-column :label="$t('tool.supervisor.command')" prop="command"></el-table-column>
<el-table-column :label="$t('tool.supervisor.dir')" prop="dir"></el-table-column>
<el-table-column :label="$t('tool.supervisor.user')" prop="user"></el-table-column>
<el-table-column :label="$t('tool.supervisor.numprocs')" prop="numprocs"></el-table-column>
<el-table-column :label="$t('commons.table.status')">
<template #default="{ row }">
<div v-if="row.status">
<el-popover placement="left" :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>{{ row.status[0].status }}</span>
</el-button>
</template>
<el-table :data="row.status">
<el-table-column
property="name"
:label="$t('commons.table.name')"
width="300"
/>
<el-table-column property="status" :label="$t('commons.table.status')" />
<el-table-column property="PID" label="PID" />
<el-table-column property="uptime" :label="$t('tool.supervisor.uptime')" />
</el-table>
</el-popover>
</div>
</template>
</el-table-column>
<fu-table-operations
:ellipsis="6"
:buttons="buttons"
:label="$t('commons.table.operate')"
:fixed="mobile ? false : 'right'"
width="350px"
fix
/>
</ComplexTable>
</template>
<ConfigSuperVisor v-if="setSuperVisor" />
</LayoutContent>
<Create ref="createRef"></Create>
<Create ref="createRef" @close="search"></Create>
<File ref="fileRef"></File>
</div>
</template>
@ -24,13 +74,24 @@ import ToolRouter from '@/views/host/tool/index.vue';
import SuperVisorStatus from './status/index.vue';
import { ref } from '@vue/runtime-core';
import ConfigSuperVisor from './config/index.vue';
import { onMounted } from 'vue';
import { computed, onMounted } from 'vue';
import Create from './create/index.vue';
import File from './file/index.vue';
import { GetSupervisorProcess, OperateSupervisorProcess } from '@/api/modules/host-tool';
import { GlobalStore } from '@/store';
import i18n from '@/lang';
import { HostTool } from '@/api/interface/host-tool';
import { MsgSuccess } from '@/utils/message';
const globalStore = GlobalStore();
const loading = ref(false);
const setSuperVisor = ref(false);
const isExistSuperVisor = ref(false);
const isRunningSuperVisor = ref(true);
const createRef = ref();
const fileRef = ref();
const data = ref();
const maskShow = ref(true);
const setting = () => {
setSuperVisor.value = true;
@ -40,9 +101,128 @@ const isExist = (isExist: boolean) => {
isExistSuperVisor.value = isExist;
};
const isRunning = (running: boolean) => {
isRunningSuperVisor.value = running;
};
const openCreate = () => {
createRef.value.acceptParams();
};
onMounted(() => {});
const search = async () => {
loading.value = true;
try {
const res = await GetSupervisorProcess();
console.log(res);
data.value = res.data;
} catch (error) {}
loading.value = false;
};
const mobile = computed(() => {
return globalStore.isMobile();
});
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 })
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {})
.finally(() => {
loading.value = false;
});
})
.catch(() => {});
} catch (error) {}
};
const getFile = (name: string, file: string) => {
fileRef.value.acceptParams(name, file, 'get');
};
const edit = (row: HostTool.SupersivorProcess) => {
createRef.value.acceptParams('edit', row);
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: function (row: HostTool.SupersivorProcess) {
edit(row);
},
},
{
label: i18n.global.t('website.proxyFile'),
click: function (row: HostTool.SupersivorProcess) {
getFile(row.name, 'config');
},
},
{
label: i18n.global.t('website.log'),
click: function (row: HostTool.SupersivorProcess) {
getFile(row.name, 'out.log');
},
},
{
label: i18n.global.t('app.start'),
click: function (row: HostTool.SupersivorProcess) {
operate('start', row.name);
},
disabled: (row: any) => {
if (row.status == undefined) {
return true;
} else {
return row.status && row.status[0].status == 'RUNNING';
}
},
},
{
label: i18n.global.t('app.stop'),
click: function (row: HostTool.SupersivorProcess) {
operate('stop', row.name);
},
disabled: (row: any) => {
if (row.status == undefined) {
return true;
}
return row.status && row.status[0].status != 'RUNNING';
},
},
{
label: i18n.global.t('commons.button.restart'),
click: function (row: HostTool.SupersivorProcess) {
operate('restart', row.name);
},
disabled: (row: any): boolean => {
if (row.status == undefined) {
return true;
}
return row.status && row.status[0].status != 'RUNNING';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: HostTool.SupersivorProcess) {
operate('delete', row.name);
},
},
];
onMounted(() => {
search();
});
</script>

View File

@ -74,7 +74,7 @@ const data = ref({
ctlExist: false,
});
const em = defineEmits(['setting', 'isExist', 'before', 'update:loading', 'update:maskShow']);
const em = defineEmits(['setting', 'isExist', 'isRunning', 'update:loading', 'update:maskShow']);
const setting = () => {
em('setting', false);
@ -85,9 +85,10 @@ const toDoc = async () => {
};
const onOperate = async (operation: string) => {
em('update:maskShow', false);
operateReq.operate = operation;
ElMessageBox.confirm(
i18n.global.t('tool.supervisor.operatorHelper', [i18n.global.t('app.' + operation)]),
i18n.global.t('tool.supervisor.operatorHelper', ['Supervisor', i18n.global.t('app.' + operation)]),
i18n.global.t('app.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
@ -97,9 +98,9 @@ const onOperate = async (operation: string) => {
)
.then(() => {
em('update:loading', true);
em('before');
OperateSupervisor(operation)
.then(() => {
em('update:maskShow', true);
getStatus();
em('update:loading', false);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
@ -108,7 +109,9 @@ const onOperate = async (operation: string) => {
em('update:loading', false);
});
})
.catch(() => {});
.catch(() => {
em('update:maskShow', true);
});
};
const getStatus = async () => {
@ -116,6 +119,7 @@ const getStatus = async () => {
em('update:loading', true);
const res = await GetSupervisorStatus();
data.value = res.data.config as HostTool.Supersivor;
em('isRunning', data.value.status === 'running');
if (!data.value.isExist || !data.value.ctlExist) {
em('isExist', false);
} else {