From 1b8ecefcfed928070556798e2e7a1b2d0923c96b Mon Sep 17 00:00:00 2001 From: dushixiang <798148596@qq.com> Date: Sun, 28 Feb 2021 23:56:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=AF=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- main.go | 43 ++++ pkg/api/job.go | 86 +++++++ pkg/api/routes.go | 10 + pkg/global/global.go | 3 + pkg/handle/runner.go | 74 +----- pkg/model/job.go | 229 +++++++++++++++++ pkg/model/user.go | 1 + web/src/App.js | 43 ++-- web/src/components/job/Job.js | 450 ++++++++++++++++++++++++++++++++++ 10 files changed, 862 insertions(+), 83 deletions(-) create mode 100644 pkg/api/job.go create mode 100644 pkg/model/job.go create mode 100644 web/src/components/job/Job.js diff --git a/README.md b/README.md index 0c82fe8..41d28c4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Next Terminal -你的下一个终端。 +下一代终端。 ![Docker image](https://github.com/dushixiang/next-terminal/workflows/Docker%20image/badge.svg?branch=master) @@ -29,6 +29,10 @@ https://next-terminal.typesafe.cn/ test/test +## 协议与条款 + +如您需要在企业网络中使用 next-terminal,建议先征求 IT 管理员的同意。下载、使用或分发 next-terminal 前,您必须同意 [协议](./LICENSE) 条款与限制。本项目不提供任何担保,亦不承担任何责任。 + ## 快速安装 - [使用docker安装](docs/install-docker.md) diff --git a/main.go b/main.go index 63546b5..59a6440 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( nested "github.com/antonfisher/nested-logrus-formatter" "github.com/labstack/gommon/log" "github.com/patrickmn/go-cache" + "github.com/robfig/cron/v3" "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/sqlite" @@ -155,6 +156,9 @@ func Run() error { if err := global.DB.AutoMigrate(&model.Num{}); err != nil { return err } + if err := global.DB.AutoMigrate(&model.Job{}); err != nil { + return err + } if len(model.FindAllTemp()) == 0 { for i := 0; i <= 30; i++ { @@ -174,6 +178,45 @@ func Run() error { } }) global.Store = global.NewStore() + global.Cron = cron.New(cron.WithSeconds()) //精确到秒 + + jobs, err := model.FindJobByFunc(model.FuncCheckAssetStatusJob) + if err != nil { + return err + } + if jobs == nil || len(jobs) == 0 { + job := model.Job{ + ID: utils.UUID(), + Name: "资产状态检测", + Func: model.FuncCheckAssetStatusJob, + Cron: "0 0 0/1 * * ?", + Status: model.JobStatusRunning, + Created: utils.NowJsonTime(), + Updated: utils.NowJsonTime(), + } + 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 + } + } loginLogs, err := model.FindAliveLoginLogs() if err != nil { diff --git a/pkg/api/job.go b/pkg/api/job.go new file mode 100644 index 0000000..c74ccfd --- /dev/null +++ b/pkg/api/job.go @@ -0,0 +1,86 @@ +package api + +import ( + "github.com/labstack/echo/v4" + "next-terminal/pkg/model" + "strconv" + "strings" +) + +func JobCreateEndpoint(c echo.Context) error { + var item model.Job + if err := c.Bind(&item); err != nil { + return err + } + + if err := model.CreateNewJob(&item); err != nil { + return err + } + return Success(c, "") +} + +func JobPagingEndpoint(c echo.Context) error { + pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex")) + pageSize, _ := strconv.Atoi(c.QueryParam("pageSize")) + name := c.QueryParam("name") + status := c.QueryParam("status") + + items, total, err := model.FindPageJob(pageIndex, pageSize, name, status) + if err != nil { + return err + } + + return Success(c, H{ + "total": total, + "items": items, + }) +} + +func JobUpdateEndpoint(c echo.Context) error { + id := c.Param("id") + + var item model.Job + if err := c.Bind(&item); err != nil { + return err + } + + if err := model.UpdateJobById(&item, id); err != nil { + return err + } + + return Success(c, nil) +} + +func JobChangeStatusEndpoint(c echo.Context) error { + id := c.Param("id") + status := c.QueryParam("status") + if err := model.ChangeJobStatusById(id, status); err != nil { + return err + } + return Success(c, "") +} + +func JobDeleteEndpoint(c echo.Context) error { + ids := c.Param("id") + + split := strings.Split(ids, ",") + for i := range split { + jobId := split[i] + if err := model.DeleteJobById(jobId); err != nil { + return err + } + } + + return Success(c, nil) +} + +func JobGetEndpoint(c echo.Context) error { + id := c.Param("id") + + item, err := model.FindJobById(id) + if err != nil { + return err + } + + return Success(c, item) +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 3caf5d6..df425a3 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -144,6 +144,16 @@ func SetupRoutes() *echo.Echo { e.GET("/overview/counter", OverviewCounterEndPoint) e.GET("/overview/sessions", OverviewSessionPoint) + jobs := e.Group("/jobs", Admin) + { + jobs.POST("", JobCreateEndpoint) + jobs.GET("/paging", JobPagingEndpoint) + jobs.PUT("/:id", JobUpdateEndpoint) + jobs.POST("/:id/change-status", JobChangeStatusEndpoint) + jobs.DELETE("/:id", JobDeleteEndpoint) + jobs.GET("/:id", JobGetEndpoint) + } + return e } diff --git a/pkg/global/global.go b/pkg/global/global.go index 8648066..78d0202 100644 --- a/pkg/global/global.go +++ b/pkg/global/global.go @@ -2,6 +2,7 @@ package global import ( "github.com/patrickmn/go-cache" + "github.com/robfig/cron/v3" "gorm.io/gorm" "next-terminal/pkg/config" ) @@ -13,3 +14,5 @@ var Cache *cache.Cache var Config *config.Config var Store *TunStore + +var Cron *cron.Cron diff --git a/pkg/handle/runner.go b/pkg/handle/runner.go index b355e04..78a0967 100644 --- a/pkg/handle/runner.go +++ b/pkg/handle/runner.go @@ -1,81 +1,23 @@ package handle import ( - "github.com/robfig/cron/v3" - "github.com/sirupsen/logrus" - "log" + "next-terminal/pkg/global" "next-terminal/pkg/guacd" "next-terminal/pkg/model" "next-terminal/pkg/utils" "os" - "strconv" - "time" ) func RunTicker() { - c := cron.New(cron.WithSeconds()) //精确到秒 + // 每隔一小时删除一次未使用的会话信息 + _, _ = global.Cron.AddJob("0 0 0/1 * * ?", model.DelUnUsedSessionJob{}) + // 每隔一小时检测一次资产状态 + //_, _ = global.Cron.AddJob("0 0 0/1 * * ?", model.CheckAssetStatusJob{}) + // 每日凌晨删除超过时长限制的会话 + //_, _ = global.Cron.AddJob("0 0 0 * * ?", model.DelTimeoutSessionJob{}) - _, _ = c.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) - } - } - } - // 每隔一小时检测一次资产是否存活 - assets, _ := model.FindAllAsset() - if assets != nil && len(assets) > 0 { - for i := range assets { - asset := assets[i] - active := utils.Tcping(asset.IP, asset.Port) - model.UpdateAssetActiveById(active, asset.ID) - logrus.Infof("资产「%v」ID「%v」存活状态检测完成,存活「%v」。", asset.Name, asset.ID, active) - } - } - }) - - _, err := c.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) - } - } - }) - - if err != nil { - log.Fatal(err) - } - - c.Start() + global.Cron.Start() } func RunDataFix() { diff --git a/pkg/model/job.go b/pkg/model/job.go new file mode 100644 index 0000000..258f6c5 --- /dev/null +++ b/pkg/model/job.go @@ -0,0 +1,229 @@ +package model + +import ( + "errors" + "github.com/robfig/cron/v3" + "github.com/sirupsen/logrus" + "next-terminal/pkg/global" + "next-terminal/pkg/utils" + "strconv" + "time" +) + +const ( + JobStatusRunning = "running" + JobStatusNotRunning = "not-running" + + FuncCheckAssetStatusJob = "check-asset-status-job" + FuncDelUnUsedSessionJob = "del-unused-session-job" + FuncDelTimeoutSessionJob = "del-timeout-session-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"` +} + +func (r *Job) TableName() string { + return "jobs" +} + +func FindPageJob(pageIndex, pageSize int, name, status string) (o []Job, total int64, err error) { + job := Job{} + db := global.DB.Table(job.TableName()) + dbCounter := global.DB.Table(job.TableName()) + + if len(name) > 0 { + db = db.Where("name like ?", "%"+name+"%") + dbCounter = dbCounter.Where("name like ?", "%"+name+"%") + } + + if len(status) > 0 { + db = db.Where("status = ?", status) + dbCounter = dbCounter.Where("status = ?", status) + } + + err = dbCounter.Count(&total).Error + if err != nil { + return nil, 0, err + } + + err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error + if o == nil { + o = make([]Job, 0) + } + return +} + +func FindJobByFunc(function string) (o []Job, err error) { + db := global.DB + err = db.Where("func = ?", function).Find(&o).Error + return +} + +func CreateNewJob(o *Job) (err error) { + + if o.Status == JobStatusRunning { + j, err := getJob(o.ID, o.Func) + if err != nil { + return err + } + jobId, err := global.Cron.AddJob(o.Cron, j) + if err != nil { + return err + } + o.JobId = int(jobId) + } + + return global.DB.Create(o).Error +} + +func UpdateJobById(o *Job, id string) (err error) { + if o.Status == JobStatusRunning { + return errors.New("请先停止定时任务后再修改") + } + + return global.DB.Where("id = ?", id).Updates(o).Error +} + +func UpdateJonUpdatedById(id string) (err error) { + err = global.DB.Where("id = ?", id).Update("updated = ?", utils.NowJsonTime()).Error + return +} + +func ChangeJobStatusById(id, status string) (err error) { + var job Job + err = global.DB.Where("id = ?", id).First(&job).Error + if err != nil { + return err + } + if status == JobStatusNotRunning { + j, err := getJob(job.ID, job.Func) + if err != nil { + return err + } + entryID, err := global.Cron.AddJob(job.Cron, j) + if err != nil { + return err + } + job.JobId = int(entryID) + return global.DB.Where("id = ?", id).Update("status = ?", JobStatusRunning).Error + } else { + global.Cron.Remove(cron.EntryID(job.JobId)) + return global.DB.Where("id = ?", id).Update("status = ?", JobStatusNotRunning).Error + } +} + +func FindJobById(id string) (o Job, err error) { + err = global.DB.Where("id = ?").First(&o).Error + return +} + +func DeleteJobById(id string) error { + job, err := FindJobById(id) + if err != nil { + return err + } + if job.Status == JobStatusRunning { + if err := ChangeJobStatusById(JobStatusNotRunning, id); err != nil { + return err + } + } + return global.DB.Where("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("未识别的任务") + } + return job, err +} + +type CheckAssetStatusJob struct { + ID string +} + +func (r CheckAssetStatusJob) Run() { + assets, _ := FindAllAsset() + if assets != nil && len(assets) > 0 { + 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) + } + } + 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 != "" { + _ = 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/user.go b/pkg/model/user.go index d26f147..3c8ccce 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -21,6 +21,7 @@ type User struct { Enabled bool `json:"enabled"` Created utils.JsonTime `json:"created"` Type string `json:"type"` + Mail string `json:"mail"` } type UserVo struct { diff --git a/web/src/App.js b/web/src/App.js index 900c512..dec53a4 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -16,10 +16,10 @@ import { AuditOutlined, BlockOutlined, CloudServerOutlined, - CodeOutlined, + CodeOutlined, ControlOutlined, DashboardOutlined, DesktopOutlined, - DisconnectOutlined, + DisconnectOutlined, GoldOutlined, IdcardOutlined, LinkOutlined, LoginOutlined, @@ -39,6 +39,7 @@ import {isAdmin} from "./service/permission"; import UserGroup from "./components/user/UserGroup"; import LoginLog from "./components/session/LoginLog"; import Term from "./components/access/Term"; +import Job from "./components/job/Job"; const {Footer, Sider} = Layout; @@ -161,23 +162,10 @@ class App extends Component { - }> - }> - - 动态指令 - - - {/*}> - - 静默指令 - - */} - - { this.state.triggerMenu && isAdmin() ? <> - }> + }> }> 在线会话 @@ -195,6 +183,28 @@ class App extends Component { 登录日志 + + + + }> + + }> + + 动态指令 + + + + {/*}> + + 静默指令 + + */} + + }> + + 定时任务 + + }> @@ -251,6 +261,7 @@ class App extends Component { +