diff --git a/main.go b/main.go index 59a6440..b824247 100644 --- a/main.go +++ b/main.go @@ -159,6 +159,9 @@ func Run() error { if err := global.DB.AutoMigrate(&model.Job{}); err != nil { return err } + if err := global.DB.AutoMigrate(&model.JobLog{}); err != nil { + return err + } if len(model.FindAllTemp()) == 0 { for i := 0; i <= 30; i++ { @@ -197,24 +200,14 @@ func Run() error { if err := model.CreateNewJob(&job); err != nil { return err } - } - - jobs, err = model.FindJobByFunc(model.FuncDelTimeoutSessionJob) - if err != nil { - return err - } - if jobs == nil || len(jobs) == 0 { - job := model.Job{ - ID: utils.UUID(), - Name: "超时会话检测", - Func: model.FuncDelTimeoutSessionJob, - Cron: "0 0 0 * * ?", - Status: model.JobStatusRunning, - Created: utils.NowJsonTime(), - Updated: utils.NowJsonTime(), - } - if err := model.CreateNewJob(&job); err != nil { - return err + } else { + for i := range jobs { + if jobs[i].Status == model.JobStatusRunning { + err := model.ChangeJobStatusById(jobs[i].ID, model.JobStatusRunning) + if err != nil { + return err + } + } } } diff --git a/pkg/api/job.go b/pkg/api/job.go index c74ccfd..8d11d50 100644 --- a/pkg/api/job.go +++ b/pkg/api/job.go @@ -60,6 +60,14 @@ func JobChangeStatusEndpoint(c echo.Context) error { return Success(c, "") } +func JobExecEndpoint(c echo.Context) error { + id := c.Param("id") + if err := model.ExecJobById(id); err != nil { + return err + } + return Success(c, "") +} + func JobDeleteEndpoint(c echo.Context) error { ids := c.Param("id") @@ -84,3 +92,22 @@ func JobGetEndpoint(c echo.Context) error { return Success(c, item) } + +func JobGetLogsEndpoint(c echo.Context) error { + id := c.Param("id") + + items, err := model.FindJobLogs(id) + if err != nil { + return err + } + + return Success(c, items) +} + +func JobDeleteLogsEndpoint(c echo.Context) error { + id := c.Param("id") + if err := model.DeleteJobLogByJobId(id); err != nil { + return err + } + return Success(c, "") +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index df425a3..bb33423 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -150,8 +150,11 @@ func SetupRoutes() *echo.Echo { jobs.GET("/paging", JobPagingEndpoint) jobs.PUT("/:id", JobUpdateEndpoint) jobs.POST("/:id/change-status", JobChangeStatusEndpoint) + jobs.POST("/:id/exec", JobExecEndpoint) jobs.DELETE("/:id", JobDeleteEndpoint) jobs.GET("/:id", JobGetEndpoint) + jobs.GET("/:id/logs", JobGetLogsEndpoint) + jobs.DELETE("/:id/logs", JobDeleteLogsEndpoint) } return e diff --git a/pkg/handle/runner.go b/pkg/handle/runner.go index 78a0967..4142f22 100644 --- a/pkg/handle/runner.go +++ b/pkg/handle/runner.go @@ -1,21 +1,61 @@ package handle import ( + "github.com/sirupsen/logrus" "next-terminal/pkg/global" "next-terminal/pkg/guacd" "next-terminal/pkg/model" "next-terminal/pkg/utils" "os" + "strconv" + "time" ) func RunTicker() { // 每隔一小时删除一次未使用的会话信息 - _, _ = global.Cron.AddJob("0 0 0/1 * * ?", model.DelUnUsedSessionJob{}) - // 每隔一小时检测一次资产状态 - //_, _ = global.Cron.AddJob("0 0 0/1 * * ?", model.CheckAssetStatusJob{}) + _, _ = global.Cron.AddFunc("0 0 0/1 * * ?", func() { + sessions, _ := model.FindSessionByStatusIn([]string{model.NoConnect, model.Connecting}) + if sessions != nil && len(sessions) > 0 { + now := time.Now() + for i := range sessions { + if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 { + _ = model.DeleteSessionById(sessions[i].ID) + s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port) + logrus.Infof("会话「%v」ID「%v」超过1小时未打开,已删除。", s, sessions[i].ID) + } + } + } + }) // 每日凌晨删除超过时长限制的会话 - //_, _ = global.Cron.AddJob("0 0 0 * * ?", model.DelTimeoutSessionJob{}) + _, _ = global.Cron.AddFunc("0 0 0 * * ?", func() { + property, err := model.FindPropertyByName("session-saved-limit") + if err != nil { + return + } + if property.Value == "" || property.Value == "-" { + return + } + limit, err := strconv.Atoi(property.Value) + if err != nil { + return + } + sessions, err := model.FindOutTimeSessions(limit) + if err != nil { + return + } + + if sessions != nil && len(sessions) > 0 { + var sessionIds []string + for i := range sessions { + sessionIds = append(sessionIds, sessions[i].ID) + } + err := model.DeleteSessionByIds(sessionIds) + if err != nil { + logrus.Errorf("删除离线会话失败 %v", err) + } + } + }) global.Cron.Start() } diff --git a/pkg/model/job.go b/pkg/model/job.go index 258f6c5..b024086 100644 --- a/pkg/model/job.go +++ b/pkg/model/job.go @@ -2,11 +2,11 @@ package model import ( "errors" + "fmt" "github.com/robfig/cron/v3" "github.com/sirupsen/logrus" "next-terminal/pkg/global" "next-terminal/pkg/utils" - "strconv" "time" ) @@ -14,20 +14,19 @@ const ( JobStatusRunning = "running" JobStatusNotRunning = "not-running" - FuncCheckAssetStatusJob = "check-asset-status-job" - FuncDelUnUsedSessionJob = "del-unused-session-job" - FuncDelTimeoutSessionJob = "del-timeout-session-job" + FuncCheckAssetStatusJob = "check-asset-status-job" ) type Job struct { - ID string `gorm:"primary_key" json:"id"` - JobId int `json:"jobId"` - Name string `json:"name"` - Func string `json:"func"` - Cron string `json:"cron"` - Status string `json:"status"` - Created utils.JsonTime `json:"created"` - Updated utils.JsonTime `json:"updated"` + ID string `gorm:"primary_key" json:"id"` + CronJobId int `json:"cronJobId"` + Name string `json:"name"` + Func string `json:"func"` + Cron string `json:"cron"` + Status string `json:"status"` + Metadata string `json:"metadata"` + Created utils.JsonTime `json:"created"` + Updated utils.JsonTime `json:"updated"` } func (r *Job) TableName() string { @@ -78,7 +77,7 @@ func CreateNewJob(o *Job) (err error) { if err != nil { return err } - o.JobId = int(jobId) + o.CronJobId = int(jobId) } return global.DB.Create(o).Error @@ -93,7 +92,7 @@ func UpdateJobById(o *Job, id string) (err error) { } func UpdateJonUpdatedById(id string) (err error) { - err = global.DB.Where("id = ?", id).Update("updated = ?", utils.NowJsonTime()).Error + err = global.DB.Updates(Job{ID: id, Updated: utils.NowJsonTime()}).Error return } @@ -103,7 +102,7 @@ func ChangeJobStatusById(id, status string) (err error) { if err != nil { return err } - if status == JobStatusNotRunning { + if status == JobStatusRunning { j, err := getJob(job.ID, job.Func) if err != nil { return err @@ -112,16 +111,29 @@ func ChangeJobStatusById(id, status string) (err error) { if err != nil { return err } - job.JobId = int(entryID) - return global.DB.Where("id = ?", id).Update("status = ?", JobStatusRunning).Error + job.CronJobId = int(entryID) + return global.DB.Updates(Job{ID: id, Status: JobStatusRunning}).Error } else { - global.Cron.Remove(cron.EntryID(job.JobId)) - return global.DB.Where("id = ?", id).Update("status = ?", JobStatusNotRunning).Error + global.Cron.Remove(cron.EntryID(job.CronJobId)) + return global.DB.Updates(Job{ID: id, Status: JobStatusNotRunning}).Error } } +func ExecJobById(id string) (err error) { + job, err := FindJobById(id) + if err != nil { + return err + } + j, err := getJob(id, job.Func) + if err != nil { + return err + } + j.Run() + return nil +} + func FindJobById(id string) (o Job, err error) { - err = global.DB.Where("id = ?").First(&o).Error + err = global.DB.Where("id = ?", id).First(&o).Error return } @@ -131,21 +143,17 @@ func DeleteJobById(id string) error { return err } if job.Status == JobStatusRunning { - if err := ChangeJobStatusById(JobStatusNotRunning, id); err != nil { + if err := ChangeJobStatusById(id, JobStatusNotRunning); err != nil { return err } } - return global.DB.Where("id = ?").Delete(Job{}).Error + return global.DB.Where("id = ?", id).Delete(Job{}).Error } func getJob(id, function string) (job cron.Job, err error) { switch function { case FuncCheckAssetStatusJob: job = CheckAssetStatusJob{ID: id} - case FuncDelUnUsedSessionJob: - job = DelUnUsedSessionJob{ID: id} - case FuncDelTimeoutSessionJob: - job = DelTimeoutSessionJob{ID: id} default: return nil, errors.New("未识别的任务") } @@ -159,71 +167,38 @@ type CheckAssetStatusJob struct { func (r CheckAssetStatusJob) Run() { assets, _ := FindAllAsset() if assets != nil && len(assets) > 0 { + + msgChan := make(chan string) for i := range assets { asset := assets[i] - active := utils.Tcping(asset.IP, asset.Port) - UpdateAssetActiveById(active, asset.ID) - logrus.Infof("资产「%v」ID「%v」存活状态检测完成,存活「%v」。", asset.Name, asset.ID, active) + go func() { + t1 := time.Now() + active := utils.Tcping(asset.IP, asset.Port) + elapsed := time.Since(t1) + msg := fmt.Sprintf("资产「%v」存活状态检测完成,存活「%v」,耗时「%v」", asset.Name, active, elapsed) + + UpdateAssetActiveById(active, asset.ID) + logrus.Infof(msg) + msgChan <- msg + }() } - } - if r.ID != "" { - _ = UpdateJonUpdatedById(r.ID) - } -} -type DelUnUsedSessionJob struct { - ID string -} - -func (r DelUnUsedSessionJob) Run() { - sessions, _ := FindSessionByStatusIn([]string{NoConnect, Connecting}) - if sessions != nil && len(sessions) > 0 { - now := time.Now() - for i := range sessions { - if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 { - _ = DeleteSessionById(sessions[i].ID) - s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port) - logrus.Infof("会话「%v」ID「%v」超过1小时未打开,已删除。", s, sessions[i].ID) + if r.ID != "" { + var message = "" + for i := 0; i < len(assets); i++ { + message += <-msgChan + "\n" } + + _ = UpdateJonUpdatedById(r.ID) + jobLog := JobLog{ + ID: utils.UUID(), + JobId: r.ID, + Timestamp: utils.NowJsonTime(), + Message: message, + } + + _ = CreateNewJobLog(&jobLog) } } - if r.ID != "" { - _ = UpdateJonUpdatedById(r.ID) - } -} -type DelTimeoutSessionJob struct { - ID string -} - -func (r DelTimeoutSessionJob) Run() { - property, err := FindPropertyByName("session-saved-limit") - if err != nil { - return - } - if property.Value == "" || property.Value == "-" { - return - } - limit, err := strconv.Atoi(property.Value) - if err != nil { - return - } - sessions, err := FindOutTimeSessions(limit) - if err != nil { - return - } - - if sessions != nil && len(sessions) > 0 { - var sessionIds []string - for i := range sessions { - sessionIds = append(sessionIds, sessions[i].ID) - } - err := DeleteSessionByIds(sessionIds) - if err != nil { - logrus.Errorf("删除离线会话失败 %v", err) - } - } - if r.ID != "" { - _ = UpdateJonUpdatedById(r.ID) - } } diff --git a/pkg/model/job_log.go b/pkg/model/job_log.go new file mode 100644 index 0000000..fb7f923 --- /dev/null +++ b/pkg/model/job_log.go @@ -0,0 +1,30 @@ +package model + +import ( + "next-terminal/pkg/global" + "next-terminal/pkg/utils" +) + +type JobLog struct { + ID string `json:"id"` + Timestamp utils.JsonTime `json:"timestamp"` + JobId string `json:"jobId"` + Message string `json:"message"` +} + +func (r *JobLog) TableName() string { + return "job_logs" +} + +func CreateNewJobLog(o *JobLog) error { + return global.DB.Create(o).Error +} + +func FindJobLogs(jobId string) (o []JobLog, err error) { + err = global.DB.Where("job_id = ?", jobId).Order("timestamp asc").Find(&o).Error + return +} + +func DeleteJobLogByJobId(jobId string) error { + return global.DB.Where("job_id = ?", jobId).Delete(JobLog{}).Error +} diff --git a/web/src/App.css b/web/src/App.css index a485e9f..976f9a0 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -90,6 +90,6 @@ border-radius: 5%; } -.monitor .ant-modal-body{ +.modal-no-padding .ant-modal-body{ padding: 0; } \ No newline at end of file diff --git a/web/src/App.js b/web/src/App.js index dec53a4..8924646 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -162,6 +162,14 @@ class App extends Component { + }> + }> + + 动态指令 + + + + { this.state.triggerMenu && isAdmin() ? <> @@ -177,6 +185,9 @@ class App extends Component { 历史会话 + + + }> }> @@ -184,25 +195,9 @@ class App extends Component { - - - }> - - }> - - 动态指令 - - - - {/*}> - - 静默指令 - - */} - }> - 定时任务 + 计划任务 diff --git a/web/src/components/access/Access.js b/web/src/components/access/Access.js index 22815b0..fb30bbc 100644 --- a/web/src/components/access/Access.js +++ b/web/src/components/access/Access.js @@ -377,10 +377,10 @@ class Access extends Component { let width = window.innerWidth; let height = window.innerHeight; - let dpi = 96; - if (protocol === 'ssh' || protocol === 'telnet') { - dpi = dpi * 2; - } + let dpi = Math.floor(window.devicePixelRatio * 96); + // if (protocol === 'ssh' || protocol === 'telnet') { + // dpi = dpi * 2; + // } let token = getToken(); diff --git a/web/src/components/job/Job.css b/web/src/components/job/Job.css new file mode 100644 index 0000000..c3d010d --- /dev/null +++ b/web/src/components/job/Job.css @@ -0,0 +1,12 @@ +.cron-log { + overflow: auto; + border: 0 none; + line-height: 23px; + padding: 15px; + margin: 0; + white-space: pre-wrap; + height: 500px; + background-color: rgb(51, 51, 51); + color: #f1f1f1; + border-radius: 0; +} \ No newline at end of file diff --git a/web/src/components/job/Job.js b/web/src/components/job/Job.js index 06d0bf0..2a36e8b 100644 --- a/web/src/components/job/Job.js +++ b/web/src/components/job/Job.js @@ -2,7 +2,6 @@ import React, {Component} from 'react'; import { Button, - Checkbox, Col, Divider, Dropdown, @@ -13,7 +12,10 @@ import { PageHeader, Row, Space, + Switch, Table, + Tag, + Timeline, Tooltip, Typography } from "antd"; @@ -30,14 +32,14 @@ import { } from '@ant-design/icons'; import {itemRender} from "../../utils/utils"; import Logout from "../user/Logout"; -import {hasPermission} from "../../service/permission"; import dayjs from "dayjs"; +import JobModal from "./JobModal"; +import './Job.css' const confirm = Modal.confirm; const {Content} = Layout; const {Title, Text} = Typography; const {Search} = Input; -const CheckboxGroup = Checkbox.Group; const routes = [ { path: '', @@ -45,7 +47,7 @@ const routes = [ }, { path: 'job', - breadcrumbName: '定时任务', + breadcrumbName: '计划任务', } ]; @@ -64,7 +66,9 @@ class Job extends Component { modalVisible: false, modalTitle: '', modalConfirmLoading: false, - selectedRowKeys: [] + selectedRow: undefined, + selectedRowKeys: [], + logs: [] }; componentDidMount() { @@ -255,10 +259,34 @@ class Job extends Component { ); } + }, { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status, record) => { + return { + let jobStatus = checked ? 'running' : 'not-running'; + let result = await request.post(`/jobs/${record['id']}/change-status?status=${jobStatus}`); + if (result['code'] === 1) { + message.success('操作成功'); + await this.loadTableData(); + } else { + message.error(result['message']); + } + }} + /> + } }, { title: '任务类型', dataIndex: 'func', - key: 'func' + key: 'func', + render: (func, record) => { + switch (func) { + case "check-asset-status-job": + return 资产状态检测 + } + } }, { title: 'cron表达式', dataIndex: 'cron', @@ -275,7 +303,7 @@ class Job extends Component { ) } }, { - title: '更新日期', + title: '最后执行日期', dataIndex: 'updated', key: 'updated', render: (text, record) => { @@ -288,20 +316,38 @@ class Job extends Component { }, { title: '操作', key: 'action', - render: (text, record) => { + render: (text, record, index) => { const menu = ( + onClick={() => this.showModal('更新计划任务', record)}>编辑 + + + + @@ -309,9 +355,26 @@ class Job extends Component { return (
- + let result = await request.post(`/jobs/${record['id']}/exec`); + if (result['code'] === 1) { + message.success('执行成功'); + await this.loadTableData(); + } else { + message.error(result['message']); + items[index]['execLoading'] = false; + this.setState({ + items: items + }); + } + }}>执行