Browse Source

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

pull/1802/head
zhengkunwang 1 year ago committed by GitHub
parent
commit
39abd4341d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 39
      backend/app/api/v1/host_tool.go
  2. 2
      backend/app/api/v1/remote_db.go
  3. 6
      backend/app/dto/request/host_tool.go
  4. 17
      backend/app/dto/response/host_tool.go
  5. 294
      backend/app/service/host_tool.go
  6. 7
      backend/i18n/lang/en.yaml
  7. 5
      backend/i18n/lang/zh-Hant.yaml
  8. 5
      backend/i18n/lang/zh.yaml
  9. 2
      backend/router/ro_host.go
  10. 93
      cmd/server/docs/docs.go
  11. 93
      cmd/server/docs/swagger.json
  12. 61
      cmd/server/docs/swagger.yaml
  13. 20
      frontend/src/api/interface/host-tool.ts
  14. 14
      frontend/src/api/modules/host-tool.ts
  15. 4
      frontend/src/lang/modules/en.ts
  16. 4
      frontend/src/lang/modules/tw.ts
  17. 4
      frontend/src/lang/modules/zh.ts
  18. 44
      frontend/src/views/host/tool/supervisor/create/index.vue
  19. 164
      frontend/src/views/host/tool/supervisor/file/index.vue
  20. 190
      frontend/src/views/host/tool/supervisor/index.vue
  21. 12
      frontend/src/views/host/tool/supervisor/status/index.vue

39
backend/app/api/v1/host_tool.go

@ -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)
}

2
backend/app/api/v1/remote_db.go

@ -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) {

6
backend/app/dto/request/host_tool.go

@ -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"`
}

17
backend/app/dto/response/host_tool.go

@ -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"`
}

294
backend/app/service/host_tool.go

@ -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)
}
}
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")
section, err := configFile.NewSection(fmt.Sprintf("program:%s", req.Name))
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 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
}
return configFile.SaveTo(path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name)))
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
}

7
backend/i18n/lang/en.yaml

@ -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 }}"

5
backend/i18n/lang/zh-Hant.yaml

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

5
backend/i18n/lang/zh.yaml

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

2
backend/router/ro_host.go

@ -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)
}
}

93
cmd/server/docs/docs.go

@ -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": [

93
cmd/server/docs/swagger.json

@ -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": [

61
cmd/server/docs/swagger.yaml

@ -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:

20
frontend/src/api/interface/host-tool.ts

@ -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;
}
}

14
frontend/src/api/modules/host-tool.ts

@ -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);
};

4
frontend/src/lang/modules/en.ts

@ -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',
},
},
};

4
frontend/src/lang/modules/tw.ts

@ -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: '運行時長',
},
},
};

4
frontend/src/lang/modules/zh.ts

@ -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: '运行时长',
},
},
};

44
frontend/src/views/host/tool/supervisor/create/index.vue

@ -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;

164
frontend/src/views/host/tool/supervisor/file/index.vue

@ -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>

190
frontend/src/views/host/tool/supervisor/index.vue

@ -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>

12
frontend/src/views/host/tool/supervisor/status/index.vue

@ -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 {

Loading…
Cancel
Save