diff --git a/controller/ServerController.go b/controller/ServerController.go index b89583d..5c9fe93 100644 --- a/controller/ServerController.go +++ b/controller/ServerController.go @@ -46,6 +46,11 @@ func (s Server) Routes() []core.Route { core.NewRoute("/server/addMonitor", http.MethodPost, s.AddMonitor).Permissions(permission.AddServerWarningRule), core.NewRoute("/server/editMonitor", http.MethodPut, s.EditMonitor).Permissions(permission.EditServerWarningRule), core.NewRoute("/server/deleteMonitor", http.MethodDelete, s.DeleteMonitor).Permissions(permission.DeleteServerWarningRule), + core.NewRoute("/server/getProcessList", http.MethodGet, s.GetProcessList).Permissions(permission.ShowServerProcessPage), + core.NewRoute("/server/addProcess", http.MethodPost, s.AddProcess).Permissions(permission.AddServerProcess), + core.NewRoute("/server/editProcess", http.MethodPut, s.EditProcess).Permissions(permission.EditServerProcess), + core.NewRoute("/server/deleteProcess", http.MethodDelete, s.DeleteProcess).Permissions(permission.DeleteServerProcess), + core.NewRoute("/server/execProcess", http.MethodPost, s.ExecProcess).Permissions(permission.ShowServerProcessPage), } } @@ -715,3 +720,165 @@ func (s Server) DeleteMonitor(gp *core.Goploy) core.Response { } return response.JSON{} } + +func (Server) GetProcessList(gp *core.Goploy) core.Response { + type ReqData struct { + ServerID int64 `json:"serverId" validate:"gt=0"` + } + + var reqData ReqData + if err := decodeQuery(gp.URLQuery, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + list, err := model.ServerProcess{ServerID: reqData.ServerID}.GetListByServerID() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + return response.JSON{ + Data: struct { + List model.ServerProcesses `json:"list"` + }{List: list}, + } +} + +func (Server) AddProcess(gp *core.Goploy) core.Response { + type ReqData struct { + ServerID int64 `json:"serverId" validate:"gt=0"` + Name string `json:"name" validate:"required"` + Status string `json:"status"` + Start string `json:"start"` + Stop string `json:"stop"` + Restart string `json:"restart"` + } + + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + id, err := model.ServerProcess{ + ServerID: reqData.ServerID, + Name: reqData.Name, + Status: reqData.Status, + Start: reqData.Start, + Stop: reqData.Stop, + Restart: reqData.Restart, + }.AddRow() + + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + + } + return response.JSON{ + Data: struct { + ID int64 `json:"id"` + }{ID: id}, + } +} + +func (Server) EditProcess(gp *core.Goploy) core.Response { + type ReqData struct { + ID int64 `json:"id" validate:"gt=0"` + Name string `json:"name" validate:"required"` + Status string `json:"status"` + Start string `json:"start"` + Stop string `json:"stop"` + Restart string `json:"restart"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + err := model.ServerProcess{ + ID: reqData.ID, + Name: reqData.Name, + Status: reqData.Status, + Start: reqData.Start, + Stop: reqData.Stop, + Restart: reqData.Restart, + }.EditRow() + + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + return response.JSON{} +} + +func (Server) DeleteProcess(gp *core.Goploy) core.Response { + type ReqData struct { + ID int64 `json:"id" validate:"gt=0"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + if err := (model.ServerProcess{ID: reqData.ID}).DeleteRow(); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + return response.JSON{} +} + +func (Server) ExecProcess(gp *core.Goploy) core.Response { + type ReqData struct { + ID int64 `json:"id" validate:"gt=0"` + Command string `json:"command" validate:"required"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + serverProcess, err := model.ServerProcess{ID: reqData.ID}.GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + server, err := (model.Server{ID: serverProcess.ServerID}).GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + script := "" + switch reqData.Command { + case "status": + script = serverProcess.Status + case "start": + script = serverProcess.Start + case "stop": + script = serverProcess.Stop + case "restart": + script = serverProcess.Restart + default: + return response.JSON{Code: response.Error, Message: "Command error"} + } + if script == "" { + return response.JSON{Code: response.Error, Message: "Command empty"} + } + + client, err := server.ToSSHConfig().Dial() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer session.Close() + + var sshOutbuf, sshErrbuf bytes.Buffer + session.Stdout = &sshOutbuf + session.Stderr = &sshErrbuf + err = session.Run(script) + core.Log(core.TRACE, fmt.Sprintf("%s exec cmd %s, result %t, stdout: %s, stderr: %s", gp.UserInfo.Name, script, err == nil, sshOutbuf.String(), sshErrbuf.String())) + return response.JSON{ + Data: struct { + ExecRes bool `json:"execRes"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + }{ExecRes: err == nil, Stdout: sshOutbuf.String(), Stderr: sshErrbuf.String()}, + } +} diff --git a/model/ServerProcessModel.go b/model/ServerProcessModel.go new file mode 100644 index 0000000..e326039 --- /dev/null +++ b/model/ServerProcessModel.go @@ -0,0 +1,120 @@ +// Copyright 2022 The Goploy Authors. All rights reserved. +// Use of this source code is governed by a GPLv3-style +// license that can be found in the LICENSE file. + +package model + +import ( + sq "github.com/Masterminds/squirrel" +) + +const serverProcessTable = "`server_process`" + +type ServerProcess struct { + ID int64 `json:"id"` + ServerID int64 `json:"serverId"` + Name string `json:"name"` + Start string `json:"start"` + Stop string `json:"stop"` + Status string `json:"status"` + Restart string `json:"restart"` + InsertTime string `json:"insertTime,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` +} + +type ServerProcesses []ServerProcess + +func (sp ServerProcess) GetData() (ServerProcess, error) { + var serverProcess ServerProcess + err := sq. + Select("id, server_id, name, start, stop, status, restart"). + From(serverProcessTable). + Where(sq.Eq{"id": sp.ID}). + OrderBy("id DESC"). + RunWith(DB). + QueryRow(). + Scan(&serverProcess.ID, + &serverProcess.ServerID, + &serverProcess.Name, + &serverProcess.Start, + &serverProcess.Stop, + &serverProcess.Status, + &serverProcess.Restart) + if err != nil { + return serverProcess, err + } + return serverProcess, nil +} + +func (sp ServerProcess) GetListByServerID() (ServerProcesses, error) { + rows, err := sq. + Select("id, server_id, name, start, stop, status, restart, insert_time, update_time"). + From(serverProcessTable). + Where(sq.Eq{"server_id": sp.ServerID}). + OrderBy("id DESC"). + RunWith(DB). + Query() + + if err != nil { + return nil, err + } + serverProcesses := ServerProcesses{} + for rows.Next() { + var serverProcess ServerProcess + + if err := rows.Scan( + &serverProcess.ID, + &serverProcess.ServerID, + &serverProcess.Name, + &serverProcess.Start, + &serverProcess.Stop, + &serverProcess.Status, + &serverProcess.Restart, + &serverProcess.InsertTime, + &serverProcess.UpdateTime, + ); err != nil { + return serverProcesses, err + } + serverProcesses = append(serverProcesses, serverProcess) + } + return serverProcesses, nil +} + +func (sp ServerProcess) AddRow() (int64, error) { + result, err := sq. + Insert(serverProcessTable). + Columns("server_id", "name", "start", "stop", "status", "restart"). + Values(sp.ServerID, sp.Name, sp.Start, sp.Stop, sp.Status, sp.Restart). + RunWith(DB). + Exec() + if err != nil { + return 0, err + } + id, err := result.LastInsertId() + return id, err +} + +func (sp ServerProcess) EditRow() error { + _, err := sq. + Update(serverProcessTable). + SetMap(sq.Eq{ + "name": sp.Name, + "start": sp.Start, + "stop": sp.Stop, + "status": sp.Status, + "restart": sp.Restart, + }). + Where(sq.Eq{"id": sp.ID}). + RunWith(DB). + Exec() + return err +} + +func (sp ServerProcess) DeleteRow() error { + _, err := sq. + Delete(serverProcessTable). + Where(sq.Eq{"id": sp.ID}). + RunWith(DB). + Exec() + return err +} diff --git a/model/sql/1.10.0.sql b/model/sql/1.10.0.sql new file mode 100644 index 0000000..7e28b32 --- /dev/null +++ b/model/sql/1.10.0.sql @@ -0,0 +1,4 @@ +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (70, 23, 'ShowServerProcessPage', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (71, 23, 'AddServerProcess', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (72, 23, 'EditServerProcess', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (73, 23, 'DeleteServerProcess', 0, ''); diff --git a/model/sql/goploy.sql b/model/sql/goploy.sql index f076d38..f72aa1c 100644 --- a/model/sql/goploy.sql +++ b/model/sql/goploy.sql @@ -420,6 +420,10 @@ INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALU INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (67, 56, 'FileCompare', 0, ''); INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (68, 56, 'FileSync', 0, ''); INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (69, 56, 'ProcessManager', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (70, 23, 'ShowServerProcessPage', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (71, 23, 'AddServerProcess', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (72, 23, 'EditServerProcess', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (73, 23, 'DeleteServerProcess', 0, ''); INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 13); INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 14); INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 15); diff --git a/permission/Permission.go b/permission/Permission.go index a46a942..4d4506e 100644 --- a/permission/Permission.go +++ b/permission/Permission.go @@ -74,4 +74,8 @@ const ( FileCompare = 67 FileSync = 68 ProcessManager = 69 + ShowServerProcessPage = 70 + AddServerProcess = 71 + EditServerProcess = 72 + DeleteServerProcess = 73 ) diff --git a/web/package.json b/web/package.json index 0e71305..26e6aab 100644 --- a/web/package.json +++ b/web/package.json @@ -4,8 +4,8 @@ "license": "GPLv3", "scripts": { "dev": "vite", - "build": "vite build", - "server": "cd ../ && go run main.go --asset-dir=./" + "server": "cd ../ && go run main.go --asset-dir=./", + "build": "vite build" }, "dependencies": { "@chenfengyuan/vue-qrcode": "^2.0.0-beta", diff --git a/web/src/api/server.ts b/web/src/api/server.ts index 509cce5..a9bd45f 100644 --- a/web/src/api/server.ts +++ b/web/src/api/server.ts @@ -292,3 +292,98 @@ export class ServerMonitorDelete extends Request { this.param = param } } + +export interface ServerProcessData { + [key: string]: any + id: number + serverId: number + name: string + start: string + stop: string + status: string + restart: string + InsertTime: string + UpdateTime: string +} + +export class ServerProcessList extends Request { + readonly url = '/server/getProcessList' + readonly method = 'get' + public param: { + serverId: number + } + + public declare datagram: { + list: ServerProcessData[] + } + constructor(param: ServerProcessList['param']) { + super() + this.param = param + } +} + +export class ServerProcessAdd extends Request { + readonly url = '/server/addProcess' + readonly method = 'post' + public param: { + name: string + start: string + stop: string + status: string + restart: string + } + public declare datagram: ID + constructor(param: ServerProcessAdd['param']) { + super() + this.param = param + } +} + +export class ServerProcessEdit extends Request { + readonly url = '/server/editProcess' + readonly method = 'put' + public param: { + id: number + name: string + start: string + stop: string + status: string + restart: string + } + constructor(param: ServerProcessEdit['param']) { + super() + this.param = param + } +} + +export class ServerProcessDelete extends Request { + readonly url = '/server/deleteProcess' + readonly method = 'delete' + public param: { + id: number + } + constructor(param: ServerProcessDelete['param']) { + super() + this.param = param + } +} + +export class ServerExecProcess extends Request { + readonly url = '/server/execProcess' + readonly method = 'post' + readonly timeout = 0 + public param: { + id: number + serverId: number + command: string + } + public declare datagram: { + execRes: boolean + stdout: string + stderr: string + } + constructor(param: ServerExecProcess['param']) { + super() + this.param = param + } +} diff --git a/web/src/icons/svg/processManage.svg b/web/src/icons/svg/processManage.svg new file mode 100644 index 0000000..665254e --- /dev/null +++ b/web/src/icons/svg/processManage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/lang/en.json b/web/src/lang/en.json index 91477bc..808069f 100644 --- a/web/src/lang/en.json +++ b/web/src/lang/en.json @@ -137,6 +137,7 @@ "serverTerminal": "Terminal", "serverSFTP": "SFTP", "serverAgent": "Monitor", + "serverProcess": "Process", "template": "Template", "crontab": "Crontab", "serverCron": "Cron", @@ -219,7 +220,11 @@ "DeployTask": "Can deploy task", "FileCompare": "File compare", "FileSync": "File sync", - "ProcessManager": "Manage process" + "ProcessManager": "Manage process", + "ShowServerProcessPage": "See the server process page", + "AddServerProcess": "Add a server process", + "EditServerProcess": "Edit the server process", + "DeleteServerProcess": "Delete the server process" }, "tagsView": { "refresh": "Refresh", @@ -268,6 +273,7 @@ "diskIO": "Disk IO", "addMonitor": "Create monitor", "monitorList": "Monitor list", + "deleteTips": "This action will delete the {name}, continue?", "removeCronTips": "This action will delete the crontab, continue?", "removeMonitorTips": "This action will delete the monitor({item}), continue?", "item": "Item", diff --git a/web/src/lang/zh-cn.json b/web/src/lang/zh-cn.json index c0e23b9..4531c74 100644 --- a/web/src/lang/zh-cn.json +++ b/web/src/lang/zh-cn.json @@ -137,8 +137,8 @@ "serverTerminal": "Terminal", "serverSFTP": "SFTP", "serverAgent": "服务器监控", + "serverProcess": "进程管理", "serverCron": "定时任务", - "template": "模板设置", "crontab": "Crontab管理", "namespace": "空间管理", "namespaceSetting": "空间设置", @@ -219,7 +219,11 @@ "DeployTask": "定时构建", "FileCompare": "文件对比", "FileSync": "文件同步", - "ProcessManager": "进程管理" + "ProcessManager": "进程管理", + "ShowServerProcessPage": "查看服务器进程管理", + "AddServerProcess": "新增服务器进程", + "EditServerProcess": "编辑服务器进程", + "DeleteServerProcess": "删除服务器进程" }, "tagsView": { "refresh": "刷新", @@ -257,6 +261,7 @@ "copyPubTips": "复制成功,请粘贴到目标服务器~/.ssh/authorized_keys里面", "testConnection": "测试连接", "removeServerTips": "此操作将删除服务器({serverName}), 是否继续?", + "deleteTips": "此操作将永久删除({name}), 是否继续?", "cpuUsage": "CPU使用率", "ramUsage": "RAM使用率", "loadavg": "系统负载", diff --git a/web/src/permission.ts b/web/src/permission.ts index 2773f59..05536be 100644 --- a/web/src/permission.ts +++ b/web/src/permission.ts @@ -68,4 +68,8 @@ export default Object.freeze({ FileCompare: 67, FileSync: 68, ProcessManager: 69, + ShowServerProcessPage: 70, + AddServerProcess: 71, + EditServerProcess: 72, + DeleteServerProcess: 73, }) diff --git a/web/src/router/asyncRoutes.ts b/web/src/router/asyncRoutes.ts index 1498406..041f3f5 100644 --- a/web/src/router/asyncRoutes.ts +++ b/web/src/router/asyncRoutes.ts @@ -126,6 +126,16 @@ export default [ permissions: [permission.ShowServerMonitorPage], }, }, + { + path: 'process', + name: 'ServerProcess', + component: () => import('@/views/server/process/index.vue'), + meta: { + title: 'serverProcess', + icon: 'processManage', + permissions: [permission.ShowServerProcessPage], + }, + }, { path: 'cron', name: 'ServerCron', diff --git a/web/src/views/server/cron/index.vue b/web/src/views/server/cron/index.vue index b0b5b01..949ef82 100644 --- a/web/src/views/server/cron/index.vue +++ b/web/src/views/server/cron/index.vue @@ -297,11 +297,15 @@ function handleEdit(data: CronData) { } function handleRemove(data: CronData) { - ElMessageBox.confirm(t('serverPage.removeUserTips'), t('tips'), { - confirmButtonText: t('confirm'), - cancelButtonText: t('cancel'), - type: 'warning', - }) + ElMessageBox.confirm( + t('serverPage.deleteTips', { name: data.command }), + t('tips'), + { + confirmButtonText: t('confirm'), + cancelButtonText: t('cancel'), + type: 'warning', + } + ) .then(() => { new CronRemove({ id: data.id }).request().then(() => { getList() @@ -324,7 +328,6 @@ function onExpressionChange() { function handlePageChange(val = 1) { pagination.value.page = val - getList() } function submit() { diff --git a/web/src/views/server/process/index.vue b/web/src/views/server/process/index.vue new file mode 100644 index 0000000..b285ac7 --- /dev/null +++ b/web/src/views/server/process/index.vue @@ -0,0 +1,418 @@ + + + + +