From 1e641d12491c85288284930faf6a8150870e1617 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 9 Jan 2022 11:54:14 +0800 Subject: [PATCH 1/4] theme-mdui by @MikoyChinese --- README.md | 6 +- cmd/dashboard/controller/common_page.go | 62 +++---- cmd/dashboard/controller/controller.go | 4 +- cmd/dashboard/controller/guest_page.go | 5 +- cmd/dashboard/controller/member_api.go | 152 +++++++++--------- cmd/dashboard/controller/member_page.go | 16 +- cmd/dashboard/controller/oauth2.go | 24 +-- cmd/dashboard/main.go | 80 ++++----- cmd/dashboard/rpc/rpc.go | 28 ++-- go.mod | 3 +- pkg/mygin/auth.go | 6 +- pkg/mygin/mygin.go | 10 +- resource/template/theme-mdui/home-table.png | Bin 22337 -> 0 bytes .../{home-card.png => screenshot.png} | Bin resource/template/theme-mdui/service.png | Bin 17123 -> 0 bytes service/rpc/auth.go | 11 +- service/rpc/nezha.go | 74 ++++----- service/{dao => singleton}/alertsentinel.go | 2 +- service/{dao => singleton}/dao.go | 4 +- service/{dao => singleton}/notification.go | 2 +- service/{dao => singleton}/servicesentinel.go | 2 +- 21 files changed, 246 insertions(+), 245 deletions(-) delete mode 100644 resource/template/theme-mdui/home-table.png rename resource/template/theme-mdui/{home-card.png => screenshot.png} (100%) delete mode 100644 resource/template/theme-mdui/service.png rename service/{dao => singleton}/alertsentinel.go (99%) rename service/{dao => singleton}/dao.go (95%) rename service/{dao => singleton}/notification.go (99%) rename service/{dao => singleton}/servicesentinel.go (99%) diff --git a/README.md b/README.md index a7eafb9..a3c41dd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
LOGO designed by 熊大 .

-    +   

:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。

@@ -17,8 +17,8 @@ | 默认主题 | DayNight [@JackieSung](https://github.com/JackieSung4ev) | hotaru | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- | | ![默认主题](resource/template/theme-default/screenshot.png) | ![daynight](resource/template/theme-daynight/screenshot.png) | | -|
默认主题魔改 [教程]
| | | -| ![默认主题魔改](https://cdn.jsdelivr.net/gh/idarku/img@main/me/1631120192341.webp) | | | +|
默认主题魔改 [教程]
| Neko Mdui [@MikoyChinese](https://github.com/MikoyChinese) | | +| ![默认主题魔改](https://cdn.jsdelivr.net/gh/idarku/img@main/me/1631120192341.webp) | ![Neko Mdui](resource/template/theme-mdui/screenshot.png) | | ## 安装脚本 diff --git a/cmd/dashboard/controller/common_page.go b/cmd/dashboard/controller/common_page.go index ba78c85..e318bf4 100644 --- a/cmd/dashboard/controller/common_page.go +++ b/cmd/dashboard/controller/common_page.go @@ -17,7 +17,7 @@ import ( "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type terminalContext struct { @@ -54,7 +54,7 @@ func (p *commonPage) issueViewPassword(c *gin.Context) { var vpf viewPasswordForm err := c.ShouldBind(&vpf) var hash []byte - if err == nil && vpf.Password != dao.Conf.Site.ViewPassword { + if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword { err = errors.New("查看密码错误") } if err == nil { @@ -69,12 +69,12 @@ func (p *commonPage) issueViewPassword(c *gin.Context) { c.Abort() return } - c.SetCookie(dao.Conf.Site.CookieName+"-vp", string(hash), 60*60*24, "", "", false, false) + c.SetCookie(singleton.Conf.Site.CookieName+"-vp", string(hash), 60*60*24, "", "", false, false) c.Redirect(http.StatusFound, c.Request.Referer()) } func (p *commonPage) checkViewPassword(c *gin.Context) { - if dao.Conf.Site.ViewPassword == "" { + if singleton.Conf.Site.ViewPassword == "" { c.Next() return } @@ -84,11 +84,11 @@ func (p *commonPage) checkViewPassword(c *gin.Context) { } // 验证查看密码 - viewPassword, _ := c.Cookie(dao.Conf.Site.CookieName + "-vp") - if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(dao.Conf.Site.ViewPassword)); err != nil { - c.HTML(http.StatusOK, "theme-"+dao.Conf.Site.Theme+"/viewpassword", mygin.CommonEnvironment(c, gin.H{ + viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp") + if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil { + c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/viewpassword", mygin.CommonEnvironment(c, gin.H{ "Title": "验证查看密码", - "CustomCode": dao.Conf.Site.CustomCode, + "CustomCode": singleton.Conf.Site.CustomCode, })) c.Abort() return @@ -98,22 +98,22 @@ func (p *commonPage) checkViewPassword(c *gin.Context) { } func (p *commonPage) service(c *gin.Context) { - dao.AlertsLock.RLock() - defer dao.AlertsLock.RUnlock() - c.HTML(http.StatusOK, "theme-"+dao.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{ + singleton.AlertsLock.RLock() + defer singleton.AlertsLock.RUnlock() + c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{ "Title": "服务状态", - "Services": dao.ServiceSentinelShared.LoadStats(), - "CycleTransferStats": dao.AlertsCycleTransferStatsStore, - "CustomCode": dao.Conf.Site.CustomCode, + "Services": singleton.ServiceSentinelShared.LoadStats(), + "CycleTransferStats": singleton.AlertsCycleTransferStatsStore, + "CustomCode": singleton.Conf.Site.CustomCode, })) } func (cp *commonPage) home(c *gin.Context) { - dao.SortedServerLock.RLock() - defer dao.SortedServerLock.RUnlock() - c.HTML(http.StatusOK, "theme-"+dao.Conf.Site.Theme+"/home", mygin.CommonEnvironment(c, gin.H{ - "Servers": dao.SortedServerList, - "CustomCode": dao.Conf.Site.CustomCode, + singleton.SortedServerLock.RLock() + defer singleton.SortedServerLock.RUnlock() + c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/home", mygin.CommonEnvironment(c, gin.H{ + "Servers": singleton.SortedServerList, + "CustomCode": singleton.Conf.Site.CustomCode, })) } @@ -142,12 +142,12 @@ func (cp *commonPage) ws(c *gin.Context) { defer conn.Close() count := 0 for { - dao.SortedServerLock.RLock() + singleton.SortedServerLock.RLock() err = conn.WriteJSON(Data{ Now: time.Now().Unix() * 1000, - Servers: dao.SortedServerList, + Servers: singleton.SortedServerList, }) - dao.SortedServerLock.RUnlock() + singleton.SortedServerLock.RUnlock() if err != nil { break } @@ -190,9 +190,9 @@ func (cp *commonPage) terminal(c *gin.Context) { var isAgent bool if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized { - dao.ServerLock.RLock() - _, hasID := dao.SecretToID[c.Request.Header.Get("Secret")] - dao.ServerLock.RUnlock() + singleton.ServerLock.RLock() + _, hasID := singleton.SecretToID[c.Request.Header.Get("Secret")] + singleton.ServerLock.RUnlock() if !hasID { mygin.ShowErrorPage(c, mygin.ErrInfo{ Code: http.StatusForbidden, @@ -225,9 +225,9 @@ func (cp *commonPage) terminal(c *gin.Context) { } isAgent = true } else { - dao.ServerLock.RLock() - server := dao.ServerList[terminal.serverID] - dao.ServerLock.RUnlock() + singleton.ServerLock.RLock() + server := singleton.ServerList[terminal.serverID] + singleton.ServerLock.RUnlock() if server == nil || server.TaskStream == nil { mygin.ShowErrorPage(c, mygin.ErrInfo{ Code: http.StatusForbidden, @@ -407,9 +407,9 @@ func (cp *commonPage) createTerminal(c *gin.Context) { return } - dao.ServerLock.RLock() - server := dao.ServerList[createTerminalReq.ID] - dao.ServerLock.RUnlock() + singleton.ServerLock.RLock() + server := singleton.ServerList[createTerminalReq.ID] + singleton.ServerLock.RUnlock() if server == nil || server.TaskStream == nil { mygin.ShowErrorPage(c, mygin.ErrInfo{ Code: http.StatusForbidden, diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 7f0cafb..d80f9e5 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -14,13 +14,13 @@ import ( "github.com/gin-gonic/gin" "github.com/naiba/nezha/pkg/mygin" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) func ServeWeb(port uint) *http.Server { gin.SetMode(gin.ReleaseMode) r := gin.Default() - if dao.Conf.Debug { + if singleton.Conf.Debug { gin.SetMode(gin.DebugMode) pprof.Register(r) } diff --git a/cmd/dashboard/controller/guest_page.go b/cmd/dashboard/controller/guest_page.go index c5db724..8512d15 100644 --- a/cmd/dashboard/controller/guest_page.go +++ b/cmd/dashboard/controller/guest_page.go @@ -4,9 +4,10 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type guestPage struct { @@ -34,7 +35,7 @@ func (gp *guestPage) serve() { func (gp *guestPage) login(c *gin.Context) { LoginType := "GitHub" RegistrationLink := "https://github.com/join" - if dao.Conf.Oauth2.Type == model.ConfigTypeGitee { + if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee { LoginType = "Gitee" RegistrationLink = "https://gitee.com/signup" } diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 854de73..195e369 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -16,7 +16,7 @@ import ( "github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type memberAPI struct { @@ -59,53 +59,53 @@ func (ma *memberAPI) delete(c *gin.Context) { var err error switch c.Param("model") { case "server": - err = dao.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error + err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error if err == nil { // 删除服务器 - dao.ServerLock.Lock() - delete(dao.SecretToID, dao.ServerList[id].Secret) - delete(dao.ServerList, id) - dao.ServerLock.Unlock() - dao.ReSortServer() + singleton.ServerLock.Lock() + delete(singleton.SecretToID, singleton.ServerList[id].Secret) + delete(singleton.ServerList, id) + singleton.ServerLock.Unlock() + singleton.ReSortServer() // 删除循环流量状态中的此服务器相关的记录 - dao.AlertsLock.Lock() - for i := 0; i < len(dao.Alerts); i++ { - if dao.AlertsCycleTransferStatsStore[dao.Alerts[i].ID] != nil { - delete(dao.AlertsCycleTransferStatsStore[dao.Alerts[i].ID].ServerName, id) - delete(dao.AlertsCycleTransferStatsStore[dao.Alerts[i].ID].Transfer, id) - delete(dao.AlertsCycleTransferStatsStore[dao.Alerts[i].ID].NextUpdate, id) + singleton.AlertsLock.Lock() + for i := 0; i < len(singleton.Alerts); i++ { + if singleton.AlertsCycleTransferStatsStore[singleton.Alerts[i].ID] != nil { + delete(singleton.AlertsCycleTransferStatsStore[singleton.Alerts[i].ID].ServerName, id) + delete(singleton.AlertsCycleTransferStatsStore[singleton.Alerts[i].ID].Transfer, id) + delete(singleton.AlertsCycleTransferStatsStore[singleton.Alerts[i].ID].NextUpdate, id) } } - dao.AlertsLock.Unlock() + singleton.AlertsLock.Unlock() // 删除服务器相关循环流量记录 - dao.DB.Unscoped().Delete(&model.Transfer{}, "server_id = ?", id) + singleton.DB.Unscoped().Delete(&model.Transfer{}, "server_id = ?", id) } case "notification": - err = dao.DB.Unscoped().Delete(&model.Notification{}, "id = ?", id).Error + err = singleton.DB.Unscoped().Delete(&model.Notification{}, "id = ?", id).Error if err == nil { - dao.OnDeleteNotification(id) + singleton.OnDeleteNotification(id) } case "monitor": - err = dao.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error + err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error if err == nil { - dao.ServiceSentinelShared.OnMonitorDelete(id) - err = dao.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error + singleton.ServiceSentinelShared.OnMonitorDelete(id) + err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error } case "cron": - err = dao.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error + err = singleton.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error if err == nil { - dao.CronLock.RLock() - defer dao.CronLock.RUnlock() - cr := dao.Crons[id] + singleton.CronLock.RLock() + defer singleton.CronLock.RUnlock() + cr := singleton.Crons[id] if cr != nil && cr.CronJobID != 0 { - dao.Cron.Remove(cr.CronJobID) + singleton.Cron.Remove(cr.CronJobID) } - delete(dao.Crons, id) + delete(singleton.Crons, id) } case "alert-rule": - err = dao.DB.Unscoped().Delete(&model.AlertRule{}, "id = ?", id).Error + err = singleton.DB.Unscoped().Delete(&model.AlertRule{}, "id = ?", id).Error if err == nil { - dao.OnDeleteAlert(id) + singleton.OnDeleteAlert(id) } } if err != nil { @@ -129,7 +129,7 @@ type searchResult struct { func (ma *memberAPI) searchServer(c *gin.Context) { var servers []model.Server likeWord := "%" + c.Query("word") + "%" - dao.DB.Select("id,name").Where("id = ? OR name LIKE ? OR tag LIKE ? OR note LIKE ?", + singleton.DB.Select("id,name").Where("id = ? OR name LIKE ? OR tag LIKE ? OR note LIKE ?", c.Query("word"), likeWord, likeWord, likeWord).Find(&servers) var resp []searchResult @@ -172,10 +172,10 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) { if s.ID == 0 { s.Secret = utils.MD5(fmt.Sprintf("%s%s%d", time.Now(), sf.Name, admin.ID)) s.Secret = s.Secret[:18] - err = dao.DB.Create(&s).Error + err = singleton.DB.Create(&s).Error } else { isEdit = true - err = dao.DB.Save(&s).Error + err = singleton.DB.Save(&s).Error } } if err != nil { @@ -186,26 +186,26 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) { return } if isEdit { - dao.ServerLock.Lock() - s.CopyFromRunningServer(dao.ServerList[s.ID]) + singleton.ServerLock.Lock() + s.CopyFromRunningServer(singleton.ServerList[s.ID]) // 如果修改了 Secret - if s.Secret != dao.ServerList[s.ID].Secret { + if s.Secret != singleton.ServerList[s.ID].Secret { // 删除旧 Secret-ID 绑定关系 - dao.SecretToID[s.Secret] = s.ID + singleton.SecretToID[s.Secret] = s.ID // 设置新的 Secret-ID 绑定关系 - delete(dao.SecretToID, dao.ServerList[s.ID].Secret) + delete(singleton.SecretToID, singleton.ServerList[s.ID].Secret) } - dao.ServerList[s.ID] = &s - dao.ServerLock.Unlock() + singleton.ServerList[s.ID] = &s + singleton.ServerLock.Unlock() } else { s.Host = &model.Host{} s.State = &model.HostState{} - dao.ServerLock.Lock() - dao.SecretToID[s.Secret] = s.ID - dao.ServerList[s.ID] = &s - dao.ServerLock.Unlock() + singleton.ServerLock.Lock() + singleton.SecretToID[s.Secret] = s.ID + singleton.ServerList[s.ID] = &s + singleton.ServerLock.Unlock() } - dao.ReSortServer() + singleton.ReSortServer() c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) @@ -238,13 +238,13 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { } if err == nil { if m.ID == 0 { - err = dao.DB.Create(&m).Error + err = singleton.DB.Create(&m).Error } else { - err = dao.DB.Save(&m).Error + err = singleton.DB.Save(&m).Error } } if err == nil { - err = dao.ServiceSentinelShared.OnMonitorUpdate(m) + err = singleton.ServiceSentinelShared.OnMonitorUpdate(m) } if err != nil { c.JSON(http.StatusOK, model.Response{ @@ -282,7 +282,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { cr.Cover = cf.Cover err = json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) } - tx := dao.DB.Begin() + tx := singleton.DB.Begin() if err == nil { if cf.ID == 0 { err = tx.Create(&cr).Error @@ -291,7 +291,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { } } if err == nil { - cr.CronJobID, err = dao.Cron.AddFunc(cr.Scheduler, dao.CronTrigger(cr)) + cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(cr)) } if err == nil { err = tx.Commit().Error @@ -306,15 +306,15 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { return } - dao.CronLock.Lock() - defer dao.CronLock.Unlock() - crOld := dao.Crons[cr.ID] + singleton.CronLock.Lock() + defer singleton.CronLock.Unlock() + crOld := singleton.Crons[cr.ID] if crOld != nil && crOld.CronJobID != 0 { - dao.Cron.Remove(crOld.CronJobID) + singleton.Cron.Remove(crOld.CronJobID) } - delete(dao.Crons, cr.ID) - dao.Crons[cr.ID] = &cr + delete(singleton.Crons, cr.ID) + singleton.Crons[cr.ID] = &cr c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, @@ -323,7 +323,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { func (ma *memberAPI) manualTrigger(c *gin.Context) { var cr model.Cron - if err := dao.DB.First(&cr, "id = ?", c.Param("id")).Error; err != nil { + if err := singleton.DB.First(&cr, "id = ?", c.Param("id")).Error; err != nil { c.JSON(http.StatusOK, model.Response{ Code: http.StatusBadRequest, Message: err.Error(), @@ -331,7 +331,7 @@ func (ma *memberAPI) manualTrigger(c *gin.Context) { return } - dao.ManualTrigger(cr) + singleton.ManualTrigger(cr) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, @@ -351,9 +351,9 @@ func (ma *memberAPI) forceUpdate(c *gin.Context) { var executeResult bytes.Buffer for i := 0; i < len(forceUpdateServers); i++ { - dao.ServerLock.RLock() - server := dao.ServerList[forceUpdateServers[i]] - dao.ServerLock.RUnlock() + singleton.ServerLock.RLock() + server := singleton.ServerList[forceUpdateServers[i]] + singleton.ServerLock.RUnlock() if server != nil && server.TaskStream != nil { if err := server.TaskStream.Send(&proto.Task{ Type: model.TaskTypeUpgrade, @@ -402,9 +402,9 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { } if err == nil { if n.ID == 0 { - err = dao.DB.Create(&n).Error + err = singleton.DB.Create(&n).Error } else { - err = dao.DB.Save(&n).Error + err = singleton.DB.Save(&n).Error } } if err != nil { @@ -414,7 +414,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { }) return } - dao.OnRefreshOrAddNotification(n) + singleton.OnRefreshOrAddNotification(n) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) @@ -453,9 +453,9 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { r.Enable = &enable r.ID = arf.ID if r.ID == 0 { - err = dao.DB.Create(&r).Error + err = singleton.DB.Create(&r).Error } else { - err = dao.DB.Save(&r).Error + err = singleton.DB.Save(&r).Error } } if err != nil { @@ -465,7 +465,7 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { }) return } - dao.OnRefreshOrAddAlert(r) + singleton.OnRefreshOrAddAlert(r) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) @@ -492,7 +492,7 @@ func (ma *memberAPI) logout(c *gin.Context) { }) return } - dao.DB.Model(admin).UpdateColumns(model.User{ + singleton.DB.Model(admin).UpdateColumns(model.User{ Token: "", TokenExpired: time.Now(), }) @@ -522,16 +522,16 @@ func (ma *memberAPI) updateSetting(c *gin.Context) { }) return } - dao.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on" - dao.Conf.Cover = sf.Cover - dao.Conf.GRPCHost = sf.GRPCHost - dao.Conf.IgnoredIPNotification = sf.IgnoredIPNotification - dao.Conf.Site.Brand = sf.Title - dao.Conf.Site.Theme = sf.Theme - dao.Conf.Site.CustomCode = sf.CustomCode - dao.Conf.Site.ViewPassword = sf.ViewPassword - dao.Conf.Oauth2.Admin = sf.Admin - if err := dao.Conf.Save(); err != nil { + singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on" + singleton.Conf.Cover = sf.Cover + singleton.Conf.GRPCHost = sf.GRPCHost + singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification + singleton.Conf.Site.Brand = sf.Title + singleton.Conf.Site.Theme = sf.Theme + singleton.Conf.Site.CustomCode = sf.CustomCode + singleton.Conf.Site.ViewPassword = sf.ViewPassword + singleton.Conf.Oauth2.Admin = sf.Admin + if err := singleton.Conf.Save(); err != nil { c.JSON(http.StatusOK, model.Response{ Code: http.StatusBadRequest, Message: fmt.Sprintf("请求错误:%s", err), diff --git a/cmd/dashboard/controller/member_page.go b/cmd/dashboard/controller/member_page.go index 577a43c..ff84ce5 100644 --- a/cmd/dashboard/controller/member_page.go +++ b/cmd/dashboard/controller/member_page.go @@ -6,7 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type memberPage struct { @@ -30,24 +30,24 @@ func (mp *memberPage) serve() { } func (mp *memberPage) server(c *gin.Context) { - dao.SortedServerLock.RLock() - defer dao.SortedServerLock.RUnlock() + singleton.SortedServerLock.RLock() + defer singleton.SortedServerLock.RUnlock() c.HTML(http.StatusOK, "dashboard/server", mygin.CommonEnvironment(c, gin.H{ "Title": "服务器管理", - "Servers": dao.SortedServerList, + "Servers": singleton.SortedServerList, })) } func (mp *memberPage) monitor(c *gin.Context) { c.HTML(http.StatusOK, "dashboard/monitor", mygin.CommonEnvironment(c, gin.H{ "Title": "服务监控", - "Monitors": dao.ServiceSentinelShared.Monitors(), + "Monitors": singleton.ServiceSentinelShared.Monitors(), })) } func (mp *memberPage) cron(c *gin.Context) { var crons []model.Cron - dao.DB.Find(&crons) + singleton.DB.Find(&crons) c.HTML(http.StatusOK, "dashboard/cron", mygin.CommonEnvironment(c, gin.H{ "Title": "计划任务", "Crons": crons, @@ -56,9 +56,9 @@ func (mp *memberPage) cron(c *gin.Context) { func (mp *memberPage) notification(c *gin.Context) { var nf []model.Notification - dao.DB.Find(&nf) + singleton.DB.Find(&nf) var ar []model.AlertRule - dao.DB.Find(&ar) + singleton.DB.Find(&ar) c.HTML(http.StatusOK, "dashboard/notification", mygin.CommonEnvironment(c, gin.H{ "Title": "报警通知", "Notifications": nf, diff --git a/cmd/dashboard/controller/oauth2.go b/cmd/dashboard/controller/oauth2.go index f9d25b4..aebe78a 100644 --- a/cmd/dashboard/controller/oauth2.go +++ b/cmd/dashboard/controller/oauth2.go @@ -15,7 +15,7 @@ import ( "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/utils" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type oauth2controller struct { @@ -28,10 +28,10 @@ func (oa *oauth2controller) serve() { } func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config { - if dao.Conf.Oauth2.Type == model.ConfigTypeGitee { + if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee { return &oauth2.Config{ - ClientID: dao.Conf.Oauth2.ClientID, - ClientSecret: dao.Conf.Oauth2.ClientSecret, + ClientID: singleton.Conf.Oauth2.ClientID, + ClientSecret: singleton.Conf.Oauth2.ClientSecret, Scopes: []string{}, Endpoint: oauth2.Endpoint{ AuthURL: "https://gitee.com/oauth/authorize", @@ -41,8 +41,8 @@ func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config } } else { return &oauth2.Config{ - ClientID: dao.Conf.Oauth2.ClientID, - ClientSecret: dao.Conf.Oauth2.ClientSecret, + ClientID: singleton.Conf.Oauth2.ClientID, + ClientSecret: singleton.Conf.Oauth2.ClientSecret, Scopes: []string{}, Endpoint: GitHubOauth2.Endpoint, } @@ -59,7 +59,7 @@ func (oa *oauth2controller) getRedirectURL(c *gin.Context) string { func (oa *oauth2controller) login(c *gin.Context) { state := utils.RandStringBytesMaskImprSrcUnsafe(6) - dao.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP()), state, 0) + singleton.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP()), state, 0) url := oa.getCommonOauth2Config(c).AuthCodeURL(state, oauth2.AccessTypeOnline) c.Redirect(http.StatusFound, url) } @@ -67,7 +67,7 @@ func (oa *oauth2controller) login(c *gin.Context) { func (oa *oauth2controller) callback(c *gin.Context) { var err error // 验证登录跳转时的 State - state, ok := dao.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP())) + state, ok := singleton.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, c.ClientIP())) if !ok || state.(string) != c.Query("state") { err = errors.New("非法的登录方式") } @@ -80,7 +80,7 @@ func (oa *oauth2controller) callback(c *gin.Context) { var client *GitHubAPI.Client if err == nil { oc := oauth2Config.Client(ctx, otk) - if dao.Conf.Oauth2.Type == model.ConfigTypeGitee { + if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee { client, err = GitHubAPI.NewEnterpriseClient("https://gitee.com/api/v5/", "https://gitee.com/api/v5/", oc) } else { client = GitHubAPI.NewClient(oc) @@ -99,7 +99,7 @@ func (oa *oauth2controller) callback(c *gin.Context) { return } var isAdmin bool - for _, admin := range strings.Split(dao.Conf.Oauth2.Admin, ",") { + for _, admin := range strings.Split(singleton.Conf.Oauth2.Admin, ",") { if admin != "" && gu.GetLogin() == admin { isAdmin = true break @@ -115,8 +115,8 @@ func (oa *oauth2controller) callback(c *gin.Context) { } user := model.NewUserFromGitHub(gu) user.IssueNewToken() - dao.DB.Save(&user) - c.SetCookie(dao.Conf.Site.CookieName, user.Token, 60*60*24, "", "", false, false) + singleton.DB.Save(&user) + c.SetCookie(singleton.Conf.Site.CookieName, user.Token, 60*60*24, "", "", false, false) c.Status(http.StatusOK) c.Writer.WriteString("") } diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 9645944..66d7f5a 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -16,7 +16,7 @@ import ( "github.com/naiba/nezha/cmd/dashboard/controller" "github.com/naiba/nezha/cmd/dashboard/rpc" "github.com/naiba/nezha/model" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) func init() { @@ -26,62 +26,62 @@ func init() { } // 初始化 dao 包 - dao.Conf = &model.Config{} - dao.Cron = cron.New(cron.WithSeconds(), cron.WithLocation(shanghai)) - dao.Crons = make(map[uint64]*model.Cron) - dao.ServerList = make(map[uint64]*model.Server) - dao.SecretToID = make(map[string]uint64) + singleton.Conf = &model.Config{} + singleton.Cron = cron.New(cron.WithSeconds(), cron.WithLocation(shanghai)) + singleton.Crons = make(map[uint64]*model.Cron) + singleton.ServerList = make(map[uint64]*model.Server) + singleton.SecretToID = make(map[string]uint64) - err = dao.Conf.Read("data/config.yaml") + err = singleton.Conf.Read("data/config.yaml") if err != nil { panic(err) } - dao.DB, err = gorm.Open(sqlite.Open("data/sqlite.db"), &gorm.Config{ + singleton.DB, err = gorm.Open(sqlite.Open("data/sqlite.db"), &gorm.Config{ CreateBatchSize: 200, }) if err != nil { panic(err) } - if dao.Conf.Debug { - dao.DB = dao.DB.Debug() + if singleton.Conf.Debug { + singleton.DB = singleton.DB.Debug() } - if dao.Conf.GRPCPort == 0 { - dao.Conf.GRPCPort = 5555 + if singleton.Conf.GRPCPort == 0 { + singleton.Conf.GRPCPort = 5555 } - dao.Cache = cache.New(5*time.Minute, 10*time.Minute) + singleton.Cache = cache.New(5*time.Minute, 10*time.Minute) initSystem() } func initSystem() { - dao.DB.AutoMigrate(model.Server{}, model.User{}, + singleton.DB.AutoMigrate(model.Server{}, model.User{}, model.Notification{}, model.AlertRule{}, model.Monitor{}, model.MonitorHistory{}, model.Cron{}, model.Transfer{}) - dao.LoadNotifications() + singleton.LoadNotifications() loadServers() //加载服务器列表 loadCrons() //加载计划任务 // 清理 服务请求记录 和 流量记录 的旧数据 - _, err := dao.Cron.AddFunc("0 30 3 * * *", cleanMonitorHistory) + _, err := singleton.Cron.AddFunc("0 30 3 * * *", cleanMonitorHistory) if err != nil { panic(err) } // 流量记录打点 - _, err = dao.Cron.AddFunc("0 0 * * * *", recordTransferHourlyUsage) + _, err = singleton.Cron.AddFunc("0 0 * * * *", recordTransferHourlyUsage) if err != nil { panic(err) } } func recordTransferHourlyUsage() { - dao.ServerLock.Lock() - defer dao.ServerLock.Unlock() + singleton.ServerLock.Lock() + defer singleton.ServerLock.Unlock() now := time.Now() nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local) var txs []model.Transfer - for id, server := range dao.ServerList { + for id, server := range singleton.ServerList { tx := model.Transfer{ ServerID: id, In: server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn), @@ -98,19 +98,19 @@ func recordTransferHourlyUsage() { if len(txs) == 0 { return } - log.Println("NEZHA>> Cron 流量统计入库", len(txs), dao.DB.Create(txs).Error) + log.Println("NEZHA>> Cron 流量统计入库", len(txs), singleton.DB.Create(txs).Error) } func cleanMonitorHistory() { // 清理无效数据 - dao.DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30)) - dao.DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)") + singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30)) + singleton.DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)") // 计算可清理流量记录的时长 var allServerKeep time.Time specialServerKeep := make(map[uint64]time.Time) var specialServerIDs []uint64 var alerts []model.AlertRule - dao.DB.Find(&alerts) + singleton.DB.Find(&alerts) for i := 0; i < len(alerts); i++ { for j := 0; j < len(alerts[i].Rules); j++ { // 是不是流量记录规则 @@ -136,31 +136,31 @@ func cleanMonitorHistory() { } } for id, couldRemove := range specialServerKeep { - dao.DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND created_at < ?", id, couldRemove) + singleton.DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND created_at < ?", id, couldRemove) } if allServerKeep.IsZero() { - dao.DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?)", specialServerIDs) + singleton.DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?)", specialServerIDs) } else { - dao.DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND created_at < ?", specialServerIDs, allServerKeep) + singleton.DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND created_at < ?", specialServerIDs, allServerKeep) } } func loadServers() { var servers []model.Server - dao.DB.Find(&servers) + singleton.DB.Find(&servers) for _, s := range servers { innerS := s innerS.Host = &model.Host{} innerS.State = &model.HostState{} - dao.ServerList[innerS.ID] = &innerS - dao.SecretToID[innerS.Secret] = innerS.ID + singleton.ServerList[innerS.ID] = &innerS + singleton.SecretToID[innerS.Secret] = innerS.ID } - dao.ReSortServer() + singleton.ReSortServer() } func loadCrons() { var crons []model.Cron - dao.DB.Find(&crons) + singleton.DB.Find(&crons) var err error errMsg := new(bytes.Buffer) for i := 0; i < len(crons); i++ { @@ -171,9 +171,9 @@ func loadCrons() { crIgnoreMap[cr.Servers[j]] = true } - cr.CronJobID, err = dao.Cron.AddFunc(cr.Scheduler, dao.CronTrigger(cr)) + cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(cr)) if err == nil { - dao.Crons[cr.ID] = &cr + singleton.Crons[cr.ID] = &cr } else { if errMsg.Len() == 0 { errMsg.WriteString("调度失败的计划任务:[") @@ -183,20 +183,20 @@ func loadCrons() { } if errMsg.Len() > 0 { msg := errMsg.String() - dao.SendNotification(msg[:len(msg)-1]+"] 这些任务将无法正常执行,请进入后点重新修改保存。", false) + singleton.SendNotification(msg[:len(msg)-1]+"] 这些任务将无法正常执行,请进入后点重新修改保存。", false) } - dao.Cron.Start() + singleton.Cron.Start() } func main() { cleanMonitorHistory() - go rpc.ServeRPC(dao.Conf.GRPCPort) + go rpc.ServeRPC(singleton.Conf.GRPCPort) serviceSentinelDispatchBus := make(chan model.Monitor) go rpc.DispatchTask(serviceSentinelDispatchBus) go rpc.DispatchKeepalive() - go dao.AlertSentinelStart() - dao.NewServiceSentinel(serviceSentinelDispatchBus) - srv := controller.ServeWeb(dao.Conf.HTTPPort) + go singleton.AlertSentinelStart() + singleton.NewServiceSentinel(serviceSentinelDispatchBus) + srv := controller.ServeWeb(singleton.Conf.HTTPPort) graceful.Graceful(func() error { return srv.ListenAndServe() }, func(c context.Context) error { diff --git a/cmd/dashboard/rpc/rpc.go b/cmd/dashboard/rpc/rpc.go index 03dfaab..2820c95 100644 --- a/cmd/dashboard/rpc/rpc.go +++ b/cmd/dashboard/rpc/rpc.go @@ -8,8 +8,8 @@ import ( "github.com/naiba/nezha/model" pb "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/dao" rpcService "github.com/naiba/nezha/service/rpc" + "github.com/naiba/nezha/service/singleton" ) func ServeRPC(port uint) { @@ -29,45 +29,45 @@ func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) { for task := range serviceSentinelDispatchBus { round := 0 endIndex := workedServerIndex - dao.SortedServerLock.RLock() + singleton.SortedServerLock.RLock() // 如果已经轮了一整圈又轮到自己,没有合适机器去请求,跳出循环 for round < 1 || workedServerIndex < endIndex { // 如果到了圈尾,再回到圈头,圈数加一,游标重置 - if workedServerIndex >= len(dao.SortedServerList) { + if workedServerIndex >= len(singleton.SortedServerList) { workedServerIndex = 0 round++ continue } // 如果服务器不在线,跳过这个服务器 - if dao.SortedServerList[workedServerIndex].TaskStream == nil { + if singleton.SortedServerList[workedServerIndex].TaskStream == nil { workedServerIndex++ continue } // 如果此任务不可使用此服务器请求,跳过这个服务器(有些 IPv6 only 开了 NAT64 的机器请求 IPv4 总会出问题) - if (task.Cover == model.MonitorCoverAll && task.SkipServers[dao.SortedServerList[workedServerIndex].ID]) || - (task.Cover == model.MonitorCoverIgnoreAll && !task.SkipServers[dao.SortedServerList[workedServerIndex].ID]) { + if (task.Cover == model.MonitorCoverAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID]) || + (task.Cover == model.MonitorCoverIgnoreAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID]) { workedServerIndex++ continue } // 找到合适机器执行任务,跳出循环 - dao.SortedServerList[workedServerIndex].TaskStream.Send(task.PB()) + singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB()) workedServerIndex++ break } - dao.SortedServerLock.RUnlock() + singleton.SortedServerLock.RUnlock() } } func DispatchKeepalive() { - dao.Cron.AddFunc("@every 60s", func() { - dao.SortedServerLock.RLock() - defer dao.SortedServerLock.RUnlock() - for i := 0; i < len(dao.SortedServerList); i++ { - if dao.SortedServerList[i] == nil || dao.SortedServerList[i].TaskStream == nil { + singleton.Cron.AddFunc("@every 60s", func() { + singleton.SortedServerLock.RLock() + defer singleton.SortedServerLock.RUnlock() + for i := 0; i < len(singleton.SortedServerList); i++ { + if singleton.SortedServerList[i] == nil || singleton.SortedServerList[i].TaskStream == nil { continue } - dao.SortedServerList[i].TaskStream.Send(&pb.Task{Type: model.TaskTypeKeepalive}) + singleton.SortedServerList[i].TaskStream.Send(&pb.Task{Type: model.TaskTypeKeepalive}) } }) } diff --git a/go.mod b/go.mod index 1091f4c..20b2847 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5 - github.com/Erope/goss v0.0.0-20211230093305-df3c03fd1ed4 // indirect + github.com/Erope/goss v0.0.0-20211230093305-df3c03fd1ed4 github.com/artdarek/go-unzip v1.0.0 github.com/blang/semver v3.5.1+incompatible github.com/creack/pty v1.1.17 @@ -25,7 +25,6 @@ require ( github.com/ory/graceful v0.1.1 github.com/p14yground/go-github-selfupdate v0.0.0-20210520015421-eddf14461293 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pkg/errors v0.9.1 // indirect github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v3 v3.21.10 github.com/spf13/pflag v1.0.5 diff --git a/pkg/mygin/auth.go b/pkg/mygin/auth.go index 8233a7f..38123f8 100644 --- a/pkg/mygin/auth.go +++ b/pkg/mygin/auth.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/naiba/nezha/model" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type AuthorizeOption struct { @@ -38,11 +38,11 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) { var isLogin bool // 用户鉴权 - token, _ := c.Cookie(dao.Conf.Site.CookieName) + token, _ := c.Cookie(singleton.Conf.Site.CookieName) token = strings.TrimSpace(token) if token != "" { var u model.User - if err := dao.DB.Where("token = ?", token).First(&u).Error; err == nil { + if err := singleton.DB.Where("token = ?", token).First(&u).Error; err == nil { isLogin = u.TokenExpired.After(time.Now()) } if isLogin { diff --git a/pkg/mygin/mygin.go b/pkg/mygin/mygin.go index 9199063..80c53a5 100644 --- a/pkg/mygin/mygin.go +++ b/pkg/mygin/mygin.go @@ -7,7 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/naiba/nezha/model" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) var adminPage = map[string]bool{ @@ -20,15 +20,15 @@ var adminPage = map[string]bool{ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H { data["MatchedPath"] = c.MustGet("MatchedPath") - data["Version"] = dao.Version - data["Conf"] = dao.Conf + data["Version"] = singleton.Version + data["Conf"] = singleton.Conf // 是否是管理页面 data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)] // 站点标题 if t, has := data["Title"]; !has { - data["Title"] = dao.Conf.Site.Brand + data["Title"] = singleton.Conf.Site.Brand } else { - data["Title"] = fmt.Sprintf("%s - %s", t, dao.Conf.Site.Brand) + data["Title"] = fmt.Sprintf("%s - %s", t, singleton.Conf.Site.Brand) } u, ok := c.Get(model.CtxKeyAuthorizedUser) if ok { diff --git a/resource/template/theme-mdui/home-table.png b/resource/template/theme-mdui/home-table.png deleted file mode 100644 index e98537dc48b8e5fcf0cab23bcf86b21deebf8df2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22337 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 KMNUMnLSTZR9DPFo diff --git a/resource/template/theme-mdui/home-card.png b/resource/template/theme-mdui/screenshot.png similarity index 100% rename from resource/template/theme-mdui/home-card.png rename to resource/template/theme-mdui/screenshot.png diff --git a/resource/template/theme-mdui/service.png b/resource/template/theme-mdui/service.png deleted file mode 100644 index 51365fe037569c184594f21f17fee2257df73c57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17123 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 diff --git a/service/rpc/auth.go b/service/rpc/auth.go index 155ce3f..88d9017 100644 --- a/service/rpc/auth.go +++ b/service/rpc/auth.go @@ -3,10 +3,11 @@ package rpc import ( "context" - "github.com/naiba/nezha/service/dao" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + + "github.com/naiba/nezha/service/singleton" ) type AuthHandler struct { @@ -32,13 +33,13 @@ func (a *AuthHandler) Check(ctx context.Context) (uint64, error) { clientSecret = value[0] } - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() - clientID, hasID := dao.SecretToID[clientSecret] + singleton.ServerLock.RLock() + defer singleton.ServerLock.RUnlock() + clientID, hasID := singleton.SecretToID[clientSecret] if !hasID { return 0, status.Errorf(codes.Unauthenticated, "客户端认证失败") } - _, hasServer := dao.ServerList[clientID] + _, hasServer := singleton.ServerList[clientID] if !hasServer { return 0, status.Errorf(codes.Unauthenticated, "客户端认证失败") } diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index f8b6811..6a4930f 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -8,7 +8,7 @@ import ( "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/utils" pb "github.com/naiba/nezha/proto" - "github.com/naiba/nezha/service/dao" + "github.com/naiba/nezha/service/singleton" ) type NezhaHandler struct { @@ -23,25 +23,25 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece } if r.GetType() == model.TaskTypeCommand { // 处理上报的计划任务 - dao.CronLock.RLock() - defer dao.CronLock.RUnlock() - cr := dao.Crons[r.GetId()] + singleton.CronLock.RLock() + defer singleton.CronLock.RUnlock() + cr := singleton.Crons[r.GetId()] if cr != nil { - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() + singleton.ServerLock.RLock() + defer singleton.ServerLock.RUnlock() if cr.PushSuccessful && r.GetSuccessful() { - dao.SendNotification(fmt.Sprintf("[任务成功] %s ,服务器:%s,日志:\n%s", cr.Name, dao.ServerList[clientID].Name, r.GetData()), false) + singleton.SendNotification(fmt.Sprintf("[任务成功] %s ,服务器:%s,日志:\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) } if !r.GetSuccessful() { - dao.SendNotification(fmt.Sprintf("[任务失败] %s ,服务器:%s,日志:\n%s", cr.Name, dao.ServerList[clientID].Name, r.GetData()), false) + singleton.SendNotification(fmt.Sprintf("[任务失败] %s ,服务器:%s,日志:\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) } - dao.DB.Model(cr).Updates(model.Cron{ + singleton.DB.Model(cr).Updates(model.Cron{ LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(r.GetDelay())), LastResult: r.GetSuccessful(), }) } } else if model.IsServiceSentinelNeeded(r.GetType()) { - dao.ServiceSentinelShared.Dispatch(dao.ReportData{ + singleton.ServiceSentinelShared.Dispatch(singleton.ReportData{ Data: r, Reporter: clientID, }) @@ -56,14 +56,14 @@ func (s *NezhaHandler) RequestTask(h *pb.Host, stream pb.NezhaService_RequestTas return err } closeCh := make(chan error) - dao.ServerLock.RLock() + singleton.ServerLock.RLock() // 修复不断的请求 task 但是没有 return 导致内存泄漏 - if dao.ServerList[clientID].TaskClose != nil { - close(dao.ServerList[clientID].TaskClose) + if singleton.ServerList[clientID].TaskClose != nil { + close(singleton.ServerList[clientID].TaskClose) } - dao.ServerList[clientID].TaskStream = stream - dao.ServerList[clientID].TaskClose = closeCh - dao.ServerLock.RUnlock() + singleton.ServerList[clientID].TaskStream = stream + singleton.ServerList[clientID].TaskClose = closeCh + singleton.ServerLock.RUnlock() return <-closeCh } @@ -74,15 +74,15 @@ func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Re return nil, err } state := model.PB2State(r) - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() - dao.ServerList[clientID].LastActive = time.Now() - dao.ServerList[clientID].State = &state + singleton.ServerLock.RLock() + defer singleton.ServerLock.RUnlock() + singleton.ServerList[clientID].LastActive = time.Now() + singleton.ServerList[clientID].State = &state // 如果从未记录过,先打点,等到小时时间点时入库 - if dao.ServerList[clientID].PrevHourlyTransferIn == 0 || dao.ServerList[clientID].PrevHourlyTransferOut == 0 { - dao.ServerList[clientID].PrevHourlyTransferIn = int64(state.NetInTransfer) - dao.ServerList[clientID].PrevHourlyTransferOut = int64(state.NetOutTransfer) + if singleton.ServerList[clientID].PrevHourlyTransferIn == 0 || singleton.ServerList[clientID].PrevHourlyTransferOut == 0 { + singleton.ServerList[clientID].PrevHourlyTransferIn = int64(state.NetInTransfer) + singleton.ServerList[clientID].PrevHourlyTransferOut = int64(state.NetOutTransfer) } return &pb.Receipt{Proced: true}, nil @@ -95,26 +95,26 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece return nil, err } host := model.PB2Host(r) - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() - if dao.Conf.EnableIPChangeNotification && - ((dao.Conf.Cover == model.ConfigCoverAll && !dao.Conf.IgnoredIPNotificationServerIDs[clientID]) || - (dao.Conf.Cover == model.ConfigCoverIgnoreAll && dao.Conf.IgnoredIPNotificationServerIDs[clientID])) && - dao.ServerList[clientID].Host != nil && - dao.ServerList[clientID].Host.IP != "" && + singleton.ServerLock.RLock() + defer singleton.ServerLock.RUnlock() + if singleton.Conf.EnableIPChangeNotification && + ((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) || + (singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) && + singleton.ServerList[clientID].Host != nil && + singleton.ServerList[clientID].Host.IP != "" && host.IP != "" && - dao.ServerList[clientID].Host.IP != host.IP { - dao.SendNotification(fmt.Sprintf( + singleton.ServerList[clientID].Host.IP != host.IP { + singleton.SendNotification(fmt.Sprintf( "[IP变更] %s ,旧IP:%s,新IP:%s。", - dao.ServerList[clientID].Name, utils.IPDesensitize(dao.ServerList[clientID].Host.IP), utils.IPDesensitize(host.IP)), true) + singleton.ServerList[clientID].Name, utils.IPDesensitize(singleton.ServerList[clientID].Host.IP), utils.IPDesensitize(host.IP)), true) } // 判断是否是机器重启,如果是机器重启要录入最后记录的流量里面 - if dao.ServerList[clientID].Host.BootTime < host.BootTime { - dao.ServerList[clientID].PrevHourlyTransferIn = dao.ServerList[clientID].PrevHourlyTransferIn - int64(dao.ServerList[clientID].State.NetInTransfer) - dao.ServerList[clientID].PrevHourlyTransferOut = dao.ServerList[clientID].PrevHourlyTransferOut - int64(dao.ServerList[clientID].State.NetOutTransfer) + if singleton.ServerList[clientID].Host.BootTime < host.BootTime { + singleton.ServerList[clientID].PrevHourlyTransferIn = singleton.ServerList[clientID].PrevHourlyTransferIn - int64(singleton.ServerList[clientID].State.NetInTransfer) + singleton.ServerList[clientID].PrevHourlyTransferOut = singleton.ServerList[clientID].PrevHourlyTransferOut - int64(singleton.ServerList[clientID].State.NetOutTransfer) } - dao.ServerList[clientID].Host = &host + singleton.ServerList[clientID].Host = &host return &pb.Receipt{Proced: true}, nil } diff --git a/service/dao/alertsentinel.go b/service/singleton/alertsentinel.go similarity index 99% rename from service/dao/alertsentinel.go rename to service/singleton/alertsentinel.go index bdd1767..2c0b17c 100644 --- a/service/dao/alertsentinel.go +++ b/service/singleton/alertsentinel.go @@ -1,4 +1,4 @@ -package dao +package singleton import ( "fmt" diff --git a/service/dao/dao.go b/service/singleton/dao.go similarity index 95% rename from service/dao/dao.go rename to service/singleton/dao.go index e049f35..f346d99 100644 --- a/service/dao/dao.go +++ b/service/singleton/dao.go @@ -1,4 +1,4 @@ -package dao +package singleton import ( "fmt" @@ -13,7 +13,7 @@ import ( pb "github.com/naiba/nezha/proto" ) -var Version = "v0.12.3" // !!记得修改 README 中的 badge 版本!! +var Version = "v0.12.4" // !!记得修改 README 中的 badge 版本!! var ( Conf *model.Config diff --git a/service/dao/notification.go b/service/singleton/notification.go similarity index 99% rename from service/dao/notification.go rename to service/singleton/notification.go index b5736e5..d9f9d08 100644 --- a/service/dao/notification.go +++ b/service/singleton/notification.go @@ -1,4 +1,4 @@ -package dao +package singleton import ( "crypto/md5" // #nosec diff --git a/service/dao/servicesentinel.go b/service/singleton/servicesentinel.go similarity index 99% rename from service/dao/servicesentinel.go rename to service/singleton/servicesentinel.go index b9cced5..f3465a0 100644 --- a/service/dao/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -1,4 +1,4 @@ -package dao +package singleton import ( "fmt" From 8015fedb37830e4be835e658c2e284707651ce11 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 9 Jan 2022 01:55:46 -0300 Subject: [PATCH 2/4] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a3c41dd..b544aa8 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ \>> [我们的用户](https://www.google.com/search?q="powered+by+哪吒监控"&filter=0) (Google) -| 默认主题 | DayNight [@JackieSung](https://github.com/JackieSung4ev) | hotaru | -| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- | -| ![默认主题](resource/template/theme-default/screenshot.png) | ![daynight](resource/template/theme-daynight/screenshot.png) | | -|
默认主题魔改 [教程]
| Neko Mdui [@MikoyChinese](https://github.com/MikoyChinese) | | -| ![默认主题魔改](https://cdn.jsdelivr.net/gh/idarku/img@main/me/1631120192341.webp) | ![Neko Mdui](resource/template/theme-mdui/screenshot.png) | | +| 默认主题 | DayNight [@JackieSung](https://github.com/JackieSung4ev) | hotaru | +| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | +| ![默认主题](resource/template/theme-default/screenshot.png) | | | +|
默认主题魔改 [教程]
|
Neko Mdui @MikoyChinese
| | +| ![默认主题魔改](https://cdn.jsdelivr.net/gh/idarku/img@main/me/1631120192341.webp) | ![Neko Mdui](resource/template/theme-mdui/screenshot.png) | | ## 安装脚本 From 239e82a34a881cf0c3bbab2ce166903cede14ce2 Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 10 Jan 2022 09:38:55 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=92=84=20mdui=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- service/singleton/{dao.go => singleton.go} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename service/singleton/{dao.go => singleton.go} (96%) diff --git a/README.md b/README.md index b544aa8..9e12274 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
LOGO designed by 熊大 .

-    +   

:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。

diff --git a/service/singleton/dao.go b/service/singleton/singleton.go similarity index 96% rename from service/singleton/dao.go rename to service/singleton/singleton.go index f346d99..f3ac1b7 100644 --- a/service/singleton/dao.go +++ b/service/singleton/singleton.go @@ -13,7 +13,7 @@ import ( pb "github.com/naiba/nezha/proto" ) -var Version = "v0.12.4" // !!记得修改 README 中的 badge 版本!! +var Version = "v0.12.5" // !!记得修改 README 中的 badge 版本!! var ( Conf *model.Config From 2add50f0c6de023c1f1bbcb6d8945af47373d415 Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 10 Jan 2022 19:27:03 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=92=84=20mdui=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: MikoyChinese <22676744+MikoyChinese@users.noreply.github.com> --- README.md | 2 +- service/singleton/singleton.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e12274..1c85e54 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
LOGO designed by 熊大 .

-    +   

:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,计划任务和在线终端。

diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index f3ac1b7..15627b1 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -13,7 +13,7 @@ import ( pb "github.com/naiba/nezha/proto" ) -var Version = "v0.12.5" // !!记得修改 README 中的 badge 版本!! +var Version = "v0.12.6" // !!记得修改 README 中的 badge 版本!! var ( Conf *model.Config