替换定时任务库

pull/21/merge
ouqiang 2017-04-13 17:35:59 +08:00
parent dfcedb7069
commit 7aecf0c228
39 changed files with 1018 additions and 967 deletions

View File

@ -1,9 +1,6 @@
package cmd package cmd
import ( import (
"github.com/go-macaron/csrf"
"github.com/go-macaron/gzip"
"github.com/go-macaron/session"
"github.com/ouqiang/gocron/modules/app" "github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/routers" "github.com/ouqiang/gocron/routers"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -14,8 +11,6 @@ import (
"os/exec" "os/exec"
"syscall" "syscall"
"github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/logger"
"github.com/go-macaron/toolbox"
"strings"
) )
// 1号进程id // 1号进程id
@ -24,9 +19,6 @@ const InitProcess = 1
// web服务器默认端口 // web服务器默认端口
const DefaultPort = 5920 const DefaultPort = 5920
// 静态文件目录
const StaticDir = "public"
var CmdWeb = cli.Command{ var CmdWeb = cli.Command{
Name: "server", Name: "server",
Usage: "start scheduler web server", Usage: "start scheduler web server",
@ -63,44 +55,11 @@ func run(ctx *cli.Context) {
// 注册路由 // 注册路由
routers.Register(m) routers.Register(m)
// 注册中间件. // 注册中间件.
registerMiddleware(m) routers.RegisterMiddleware(m)
port := parsePort(ctx) port := parsePort(ctx)
m.Run(port) m.Run(port)
} }
// 中间件注册
func registerMiddleware(m *macaron.Macaron) {
m.Use(macaron.Logger())
m.Use(macaron.Recovery())
m.Use(gzip.Gziper())
m.Use(macaron.Static(StaticDir))
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "templates",
Extensions: []string{".html"},
// 模板语法分隔符,默认为 ["{{", "}}"]
Delims: macaron.Delims{"{{{", "}}}"},
// 追加的 Content-Type 头信息,默认为 "UTF-8"
Charset: "UTF-8",
// 渲染具有缩进格式的 JSON默认为不缩进
IndentJSON: true,
// 渲染具有缩进格式的 XML默认为不缩进
IndentXML: true,
}))
m.Use(session.Sessioner())
m.Use(csrf.Csrfer())
m.Use(toolbox.Toolboxer(m))
// 系统未安装,重定向到安装页面
m.Use(func(ctx *macaron.Context) {
installUrl := "/install"
if strings.HasPrefix(ctx.Req.RequestURI, installUrl) {
return
}
if !app.Installed {
ctx.Redirect(installUrl)
}
})
}
// 解析端口 // 解析端口
func parsePort(ctx *cli.Context) int { func parsePort(ctx *cli.Context) int {
var port int = DefaultPort var port int = DefaultPort

View File

@ -33,6 +33,12 @@ func (host *Host) Delete(id int) (int64, error) {
return Db.Id(id).Delete(host) return Db.Id(id).Delete(host)
} }
func (host *Host) NameExists(name string) (bool, error) {
count, err := Db.Where("name = ?", name).Count(host);
return count > 0, err
}
func (host *Host) List() ([]Host, error) { func (host *Host) List() ([]Host, error) {
host.parsePageAndPageSize() host.parsePageAndPageSize()
list := make([]Host, 0) list := make([]Host, 0)
@ -49,8 +55,6 @@ func (host *Host) AllList() ([]Host, error) {
return list, err return list, err
} }
func (host *Host) Total() (int64, error) { func (host *Host) Total() (int64, error) {
return Db.Count(host) return Db.Count(host)
} }

View File

@ -29,6 +29,8 @@ const (
MaxPageSize = 100000 // 每次最多取多少条 MaxPageSize = 100000 // 每次最多取多少条
) )
const DefaultTimeFormat = "2006-01-02 15:04:05"
// 创建Db // 创建Db
func CreateDb(config map[string]string) *xorm.Engine { func CreateDb(config map[string]string) *xorm.Engine {
dsn := getDbEngineDSN(config["engine"], config) dsn := getDbEngineDSN(config["engine"], config)

View File

@ -4,18 +4,11 @@ import (
"time" "time"
) )
type Protocol int8 type TaskProtocol int8
type TaskType int8
const ( const (
HTTP Protocol = iota + 1 // HTTP协议 TaskHTTP TaskProtocol = iota + 1 // HTTP协议
SSHCommand // SSHM命令 TaskSSH // SSH命令
)
const (
Timing TaskType = iota + 1 // 定时任务
Delay // 延时任务
) )
// 任务 // 任务
@ -23,12 +16,10 @@ type Task struct {
Id int `xorm:"int pk autoincr"` Id int `xorm:"int pk autoincr"`
Name string `xorm:"varchar(64) notnull"` // 任务名称 Name string `xorm:"varchar(64) notnull"` // 任务名称
Spec string `xorm:"varchar(64) notnull"` // crontab Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol Protocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command
Type TaskType `xorm:"tinyint notnull default 1"` // 任务类型 1: 定时任务 2: 延时任务
Command string `xorm:"varchar(512) notnull"` // URL地址或shell命令 Command string `xorm:"varchar(512) notnull"` // URL地址或shell命令
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制 Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
Delay int `xorm:"int notnull default 0"` // 延时任务,延时时间(单位秒) HostId int16 `xorm:"smallint notnull default 0"` // SSH host id
HostId int16 `xorm:"smallint notnull default 0"` // SSH host id
Remark string `xorm:"varchar(512) notnull default ''"` // 备注 Remark string `xorm:"varchar(512) notnull default ''"` // 备注
Created time.Time `xorm:"datetime notnull created"` // 创建时间 Created time.Time `xorm:"datetime notnull created"` // 创建时间
Deleted time.Time `xorm:"datetime deleted"` // 删除时间 Deleted time.Time `xorm:"datetime deleted"` // 删除时间
@ -43,6 +34,7 @@ type TaskHost struct {
Port int Port int
Username string Username string
Password string Password string
Alias string
} }
func (TaskHost) TableName() string { func (TaskHost) TableName() string {
@ -51,8 +43,6 @@ func (TaskHost) TableName() string {
// 新增 // 新增
func (task *Task) Create() (insertId int, err error) { func (task *Task) Create() (insertId int, err error) {
task.Status = Enabled
_, err = Db.Insert(task) _, err = Db.Insert(task)
if err == nil { if err == nil {
insertId = task.Id insertId = task.Id
@ -85,41 +75,55 @@ func (task *Task) ActiveList() ([]TaskHost, error) {
task.parsePageAndPageSize() task.parsePageAndPageSize()
list := make([]TaskHost, 0) list := make([]TaskHost, 0)
fields := "t.*, host.name,host.username,host.password,host.port" fields := "t.*, host.name,host.username,host.password,host.port"
err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Where("status = ?", Enabled).Cols(fields).Find(&list) err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Where("t.status = ?", Enabled).Cols(fields).Find(&list)
return list, err return list, err
} }
func(task *Task) Detail(id int) error { // 判断主机id是否有引用
list := make([]TaskHost, 0) func (task *Task) HostIdExist(hostId int16) (bool, error) {
fields := "t.*, host.name,host.username,host.password,host.port" count, err := Db.Where("host_id = ?", hostId).Count(task);
err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Cols(fields).Find(list)
return err return count > 0, err
}
// 判断任务名称是否存在
func (task *Task) NameExist(name string) (bool, error) {
count, err := Db.Where("name = ? AND status = ?", name, Enabled).Count(task);
return count > 0, err
}
func(task *Task) Detail(id int) (TaskHost, error) {
taskHost := TaskHost{}
fields := "t.*, host.name,host.username,host.password,host.port"
_, err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Where("t.id=?", id).Cols(fields).Get(&taskHost)
return taskHost, err
} }
func (task *Task) List() ([]TaskHost, error) { func (task *Task) List() ([]TaskHost, error) {
task.parsePageAndPageSize() task.parsePageAndPageSize()
list := make([]TaskHost, 0) list := make([]TaskHost, 0)
fields := "t.*, host.name" fields := "t.*, host.alias"
err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Cols(fields).Desc("t.id").Limit(task.PageSize, task.pageLimitOffset()).Find(&list) err := Db.Alias("t").Join("LEFT", "host", "t.host_id=host.id").Cols(fields).Desc("t.id").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
return list, err return list, err
} }
func (taskLog *TaskLog) Total() (int64, error) { func (task *Task) Total() (int64, error) {
return Db.Count(taskLog) return Db.Count(task)
} }
func (taskLog *TaskLog) parsePageAndPageSize() { func (task *Task) parsePageAndPageSize() {
if taskLog.Page <= 0 { if task.Page <= 0 {
taskLog.Page = Page task.Page = Page
} }
if taskLog.PageSize >= 0 || taskLog.PageSize > MaxPageSize { if task.PageSize >= 0 || task.PageSize > MaxPageSize {
taskLog.PageSize = PageSize task.PageSize = PageSize
} }
} }
func (taskLog *TaskLog) pageLimitOffset() int { func (task *Task) pageLimitOffset() int {
return (taskLog.Page - 1) * taskLog.PageSize return (task.Page - 1) * task.PageSize
} }

View File

@ -6,24 +6,23 @@ import (
// 任务执行日志 // 任务执行日志
type TaskLog struct { type TaskLog struct {
Id int `xorm:"int pk autoincr"` Id int64 `xorm:"bigint pk autoincr"`
taskId int `xorm:"int notnull index default 0"` // 任务id
Name string `xorm:"varchar(64) notnull"` // 任务名称 Name string `xorm:"varchar(64) notnull"` // 任务名称
Spec string `xorm:"varchar(64) notnull"` // crontab Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol Protocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command
Type TaskType `xorm:"tinyint notnull default 1"` // 任务类型 1: 定时任务 2: 延时任务
Command string `xorm:"varchar(512) notnull"` // URL地址或shell命令 Command string `xorm:"varchar(512) notnull"` // URL地址或shell命令
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制 Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
Delay int `xorm:"int notnull default 0"` // 延时任务,延时时间(单位秒)
Hostname string `xorm:"varchar(512) notnull defalut '' "` // SSH主机名逗号分隔 Hostname string `xorm:"varchar(512) notnull defalut '' "` // SSH主机名逗号分隔
StartTime time.Time `xorm:"datetime created"` // 开始执行时间 StartTime time.Time `xorm:"datetime created"` // 开始执行时间
EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间 EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间
Status Status `xorm:"tinyint notnull default 1"` // 状态 1:执行中 2:执行完毕 0:执行失败 Status Status `xorm:"tinyint notnull default 1"` // 状态 1:执行中 2:执行完毕 0:执行失败 -1 待执行
Result string `xorm:"varchar(65535) notnull defalut '' "` // 执行结果 Result string `xorm:"varchar(65535) notnull defalut '' "` // 执行结果
Page int `xorm:"-"` Page int `xorm:"-"`
PageSize int `xorm:"-"` PageSize int `xorm:"-"`
} }
func (taskLog *TaskLog) Create() (insertId int, err error) { func (taskLog *TaskLog) Create() (insertId int64, err error) {
taskLog.Status = Running taskLog.Status = Running
_, err = Db.Insert(taskLog) _, err = Db.Insert(taskLog)
@ -35,11 +34,11 @@ func (taskLog *TaskLog) Create() (insertId int, err error) {
} }
// 更新 // 更新
func (taskLog *TaskLog) Update(id int, data CommonMap) (int64, error) { func (taskLog *TaskLog) Update(id int64, data CommonMap) (int64, error) {
return Db.Table(taskLog).ID(id).Update(data) return Db.Table(taskLog).ID(id).Update(data)
} }
func (taskLog *TaskLog) setStatus(id int, status Status) (int64, error) { func (taskLog *TaskLog) setStatus(id int64, status Status) (int64, error) {
return taskLog.Update(id, CommonMap{"status": status}) return taskLog.Update(id, CommonMap{"status": status})
} }
@ -51,19 +50,24 @@ func (taskLog *TaskLog) List() ([]TaskLog, error) {
return list, err return list, err
} }
func (task *Task) Total() (int64, error) { // 清空表
return Db.Count(task) func (TaskLog *TaskLog) Clear() (int64, error) {
return Db.Where("1=1").Delete(TaskLog);
} }
func (task *Task) parsePageAndPageSize() { func (taskLog *TaskLog) Total() (int64, error) {
if task.Page <= 0 { return Db.Count(taskLog)
task.Page = Page }
func (taskLog *TaskLog) parsePageAndPageSize() {
if taskLog.Page <= 0 {
taskLog.Page = Page
} }
if task.PageSize >= 0 || task.PageSize > MaxPageSize { if taskLog.PageSize >= 0 || taskLog.PageSize > MaxPageSize {
task.PageSize = PageSize taskLog.PageSize = PageSize
} }
} }
func (task *Task) pageLimitOffset() int { func (taskLog *TaskLog) pageLimitOffset() int {
return (task.Page - 1) * task.PageSize return (taskLog.Page - 1) * taskLog.PageSize
} }

View File

@ -4,7 +4,6 @@ import (
"os" "os"
"github.com/ouqiang/gocron/models" "github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/crontask"
"github.com/ouqiang/gocron/service" "github.com/ouqiang/gocron/service"
"github.com/ouqiang/gocron/modules/setting" "github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/logger"
@ -67,7 +66,6 @@ func CreateInstallLock() error {
// 初始化资源 // 初始化资源
func InitResource() { func InitResource() {
// 初始化定时任务 // 初始化定时任务
crontask.DefaultCronTask = crontask.NewCronTask()
serviceTask := new(service.Task) serviceTask := new(service.Task)
serviceTask.Initialize() serviceTask.Initialize()
} }

View File

@ -1,94 +0,0 @@
package crontask
import (
"errors"
"github.com/robfig/cron"
"strings"
"sync"
)
var DefaultCronTask *CronTask
type CronMap map[string]*cron.Cron
type CronTask struct {
sync.RWMutex
tasks CronMap
}
func NewCronTask() *CronTask {
return &CronTask{
sync.RWMutex{},
make(CronMap),
}
}
// 新增定时任务,如果name存在则添加失败
// name 任务名称
// spec crontab时间格式定义 可定义多个时间\n分隔
func (cronTask *CronTask) Add(name string, spec string, cmd cron.FuncJob) (err error) {
if name == "" || spec == "" || cmd == nil {
return errors.New("参数不完整")
}
if cronTask.IsExist(name) {
return errors.New("任务已存在")
}
spec = strings.TrimSpace(spec)
cronTask.Lock()
defer cronTask.Unlock()
cronTask.tasks[name] = cron.New()
specs := strings.Split(spec, "|||")
for _, item := range specs {
_, err = cron.Parse(item)
if err != nil {
return err
}
}
for _, item := range specs {
err = cronTask.tasks[name].AddFunc(item, cmd)
}
cronTask.tasks[name].Start()
return err
}
// 任务不存在则新增,任务已存在则删除后新增
func (cronTask *CronTask) Update(name string, spec string, cmd cron.FuncJob) error {
if cronTask.IsExist(name) {
cronTask.Delete(name)
}
return cronTask.Add(name, spec, cmd)
}
// 判断任务是否存在
func (cronTask *CronTask) IsExist(name string) bool {
cronTask.RLock()
defer cronTask.RUnlock()
_, ok := cronTask.tasks[name]
return ok
}
// 停止任务
func (cronTask *CronTask) Stop(name string) {
if cronTask.IsExist(name) {
cronTask.tasks[name].Stop()
}
}
// 删除任务
func (cronTask *CronTask) Delete(name string) {
cronTask.Stop(name)
cronTask.Lock()
defer cronTask.Unlock()
delete(cronTask.tasks, name)
}
// 删除所有任务
func(cronTask *CronTask) DeleteAll() {
for taskName, _ := range(cronTask.tasks) {
cronTask.Delete(taskName)
}
}

View File

@ -13,20 +13,33 @@ type response struct {
Data interface{} `json:"data"` // 数据 Data interface{} `json:"data"` // 数据
} }
type Json struct{} type JsonResponse struct{}
const ResponseSuccess = 0 const ResponseSuccess = 0
const ResponseFailure = 1 const ResponseFailure = 1
const NotFound = 2
const AuthError = 3
const ServerError = 4
func (j *Json) Success(message string, data interface{}) string { const SuccessContent = "操作成功"
const FailureContent = "操作失败"
func (j *JsonResponse) Success(message string, data interface{}) string {
return j.response(ResponseSuccess, message, data) return j.response(ResponseSuccess, message, data)
} }
func (j *Json) Failure(code int, message string) string { func (j *JsonResponse) Failure(code int, message string) string {
return j.response(code, message, nil) return j.response(code, message, nil)
} }
func (j *Json) response(code int, message string, data interface{}) string { func (j *JsonResponse) CommonFailure(message string, err... error) string {
if len(err) > 0 {
logger.Warn(err)
}
return j.Failure(ResponseFailure, message)
}
func (j *JsonResponse) response(code int, message string, data interface{}) string {
resp := response{ resp := response{
Code: code, Code: code,
Message: message, Message: message,

View File

@ -6,22 +6,24 @@ function Util() {
var util = {}; var util = {};
util.post = function(url, params, callback) { util.post = function(url, params, callback) {
// 用户认证失败 // 用户认证失败
var AUTH_ERROR = -1;
var FAILURE = 1;
var SUCCESS = 0; var SUCCESS = 0;
var FAILURE = 1;
var NOT_FOUND = 2;
var AUTH_ERROR = 3;
var FAILURE_MESSAGE = '操作失败'; var FAILURE_MESSAGE = '操作失败';
$.post( $.post(
url, url,
params, params,
function(response) { function(response) {
if (!response) {
}
if (response.code === undefined) { if (response.code === undefined) {
swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error'); swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error');
} }
if (response.code == AUTH_ERROR) { if (response.code == AUTH_ERROR) {
swal(FAILURE_MESSAGE, '请登录后操作', 'error'); swal(FAILURE_MESSAGE, response.message, 'error');
return;
}
if (response.code == NOT_FOUND) {
swal(FAILURE_MESSAGE, response.message, 'error');
return; return;
} }
if (response.code == FAILURE) { if (response.code == FAILURE) {
@ -33,6 +35,36 @@ function Util() {
'json' 'json'
) )
}; };
util.confirm = function(message, callback) {
swal({
title: '操作确认',
text: message,
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
confirmButtonText: '删除',
cancelButtonColor: '#d33',
cancelButtonText: "取消",
closeOnConfirm: false,
closeOnCancel: true
},
function(isConfirm) {
if (!isConfirm) {
return;
}
callback();
}
);
};
util.removeConfirm = function(url) {
util.confirm("确定要删除吗?", function () {
util.post(url, {}, function () {
location.reload();
});
});
};
return util; return util;
} }
var util = new Util();

8
public/resource/javascript/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
package delaytask
import "gopkg.in/macaron.v1"
// 创建延时任务
func Create(ctx *macaron.Context) {
}

9
routers/home.go Normal file
View File

@ -0,0 +1,9 @@
package routers
import "gopkg.in/macaron.v1"
// 首页
func Home(ctx *macaron.Context) {
ctx.Data["Title"] = "首页"
ctx.HTML(200, "home/index")
}

View File

@ -5,6 +5,7 @@ import (
"github.com/ouqiang/gocron/models" "github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/utils" "github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/logger"
"strconv"
) )
func Index(ctx *macaron.Context) { func Index(ctx *macaron.Context) {
@ -14,40 +15,68 @@ func Index(ctx *macaron.Context) {
logger.Error(err) logger.Error(err)
} }
ctx.Data["Title"] = "主机列表" ctx.Data["Title"] = "主机列表"
ctx.Data["URI"] = "/host"
ctx.Data["Hosts"] = hosts ctx.Data["Hosts"] = hosts
ctx.HTML(200, "host/index") ctx.HTML(200, "host/index")
} }
func Create(ctx *macaron.Context) { func Create(ctx *macaron.Context) {
ctx.Data["Title"] = "添加主机" ctx.Data["Title"] = "添加主机"
ctx.Data["URI"] = "/host/create"
ctx.HTML(200, "host/create") ctx.HTML(200, "host/create")
} }
type HostForm struct { type HostForm struct {
Name string `binding:"Required"` Name string `binding:"Required;MaxSize(100)"`
Alias string `binding:"Required"` Alias string `binding:"Required;MaxSize(32)"`
Username string `binding:"Required"` Username string `binding:"Required;MaxSize(32)"`
Password string Password string `binding:"Required;MaxSize(64)"`
Port int `binding:"Required;Range(1-65535)"` Port int `binding:"Required;Range(1-65535)"`
Remark string `binding:"Required"` Remark string
} }
func Store(ctx *macaron.Context, form HostForm) string { func Store(ctx *macaron.Context, form HostForm) string {
json := utils.Json{} json := utils.JsonResponse{}
hostModel := new(models.Host) hostModel := new(models.Host)
nameExist, err := hostModel.NameExists(form.Name)
if err != nil {
return json.CommonFailure("操作失败", err)
}
if nameExist {
return json.CommonFailure("主机名已存在")
}
hostModel.Name = form.Name hostModel.Name = form.Name
hostModel.Alias = form.Alias hostModel.Alias = form.Alias
hostModel.Username = form.Username hostModel.Username = form.Username
hostModel.Password = form.Password hostModel.Password = form.Password
hostModel.Port = form.Port hostModel.Port = form.Port
hostModel.Remark = form.Remark hostModel.Remark = form.Remark
_, err := hostModel.Create() _, err = hostModel.Create()
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("保存失败", err)
return json.Failure(utils.ResponseFailure, "保存失败")
} }
return json.Success("保存成功", nil) return json.Success("保存成功", nil)
}
func Remove(ctx *macaron.Context) string {
id, err := strconv.Atoi(ctx.Params(":id"))
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("参数错误", err)
}
taskModel := new(models.Task)
exist,err := taskModel.HostIdExist(int16(id))
if err != nil {
return json.CommonFailure("操作失败", err)
}
if exist {
return json.CommonFailure("有任务引用此主机,不能删除")
}
hostModel := new(models.Host)
_, err =hostModel.Delete(id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
return json.Success("操作成功", nil)
} }

View File

@ -14,16 +14,16 @@ import (
// 系统安装 // 系统安装
type InstallForm struct { type InstallForm struct {
DbType string `binding:"IN(mysql)"` DbType string `binding:"In(mysql)"`
DbHost string `binding:"Required"` DbHost string `binding:"Required;MaxSize(50)"`
DbPort int `binding:"Required;Range(1,65535)"` DbPort int `binding:"Required;Range(1,65535)"`
DbUsername string `binding:"Required"` DbUsername string `binding:"Required;MaxSize(50)"`
DbPassword string `binding:"Required"` DbPassword string `binding:"Required;MaxSize(30)"`
DbName string `binding:"Required"` DbName string `binding:"Required;MaxSize(50)"`
DbTablePrefix string DbTablePrefix string `binding:"MinSize(20)"`
AdminUsername string `binding:"Required;MinSize(3)"` AdminUsername string `binding:"Required;MaxSize(3)"`
AdminPassword string `binding:"Required;MinSize(6)"` AdminPassword string `binding:"Required;MaxSize(6)"`
AdminEmail string `binding:"Email"` AdminEmail string `binding:"Required;Email;MaxSize(50)"`
} }
func(f InstallForm) Error(ctx *macaron.Context, errs binding.Errors) { func(f InstallForm) Error(ctx *macaron.Context, errs binding.Errors) {
@ -41,21 +41,18 @@ func Create(ctx *macaron.Context) {
// 安装 // 安装
func Store(ctx *macaron.Context, form InstallForm) string { func Store(ctx *macaron.Context, form InstallForm) string {
json := utils.Json{} json := utils.JsonResponse{}
if app.Installed { if app.Installed {
logger.Warn("系统重复安装") return json.CommonFailure("系统已安装!")
return json.Failure(utils.ResponseFailure, "系统已安装!")
} }
err := testDbConnection(form) err := testDbConnection(form)
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("数据库连接失败", err)
return json.Failure(utils.ResponseFailure, "数据库连接失败")
} }
// 写入数据库配置 // 写入数据库配置
err = writeConfig(form) err = writeConfig(form)
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("数据库配置写入文件失败", err)
return json.Failure(utils.ResponseFailure, "数据库配置写入文件失败")
} }
app.InitDb() app.InitDb()
@ -63,22 +60,19 @@ func Store(ctx *macaron.Context, form InstallForm) string {
migration := new(models.Migration) migration := new(models.Migration)
err = migration.Exec(form.DbName) err = migration.Exec(form.DbName)
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("创建数据库表失败", err)
return json.Failure(utils.ResponseFailure, "创建数据库表失败")
} }
// 创建管理员账号 // 创建管理员账号
err = createAdminUser(form) err = createAdminUser(form)
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("创建管理员账号失败", err)
return json.Failure(utils.ResponseFailure, "创建管理员账号失败")
} }
// 创建安装锁 // 创建安装锁
err = app.CreateInstallLock() err = app.CreateInstallLock()
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("创建文件安装锁失败", err)
return json.Failure(utils.ResponseFailure, "创建文件安装锁失败")
} }
app.Installed = true app.Installed = true

View File

@ -7,26 +7,45 @@ import (
"github.com/ouqiang/gocron/routers/task" "github.com/ouqiang/gocron/routers/task"
"github.com/ouqiang/gocron/routers/host" "github.com/ouqiang/gocron/routers/host"
"github.com/ouqiang/gocron/routers/tasklog" "github.com/ouqiang/gocron/routers/tasklog"
"runtime" "github.com/ouqiang/gocron/modules/utils"
"strconv" "github.com/go-macaron/session"
"github.com/go-macaron/csrf"
"github.com/go-macaron/toolbox"
"github.com/go-macaron/gzip"
"strings"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/routers/api/v1/delaytask"
) )
// 静态文件目录
const StaticDir = "public"
// 路由注册 // 路由注册
func Register(m *macaron.Macaron) { func Register(m *macaron.Macaron) {
// 所有GET方法自动注册HEAD方法 // 所有GET方法自动注册HEAD方法
m.SetAutoHead(true) m.SetAutoHead(true)
// 404错误 // 404错误
m.NotFound(func(ctx *macaron.Context) { m.NotFound(func(ctx *macaron.Context) {
ctx.HTML(404, "error/404") if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "404 - NOT FOUND"
ctx.HTML(404, "error/404")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.NotFound, "您访问的地址不存在")))
}
}) })
// 50x错误 // 50x错误
m.InternalServerError(func(ctx *macaron.Context) { m.InternalServerError(func(ctx *macaron.Context) {
ctx.HTML(500, "error/500") if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "500 - SERVER INTERNAL ERROR"
ctx.HTML(500, "error/500")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.ServerError, "网站暂时无法访问,请稍后再试")))
}
}) })
// 首页 // 首页
m.Get("/", func(ctx *macaron.Context) string { m.Get("/", Home)
return "go home"
})
// 系统安装 // 系统安装
m.Group("/install", func() { m.Group("/install", func() {
m.Get("", install.Create) m.Get("", install.Create)
@ -38,19 +57,16 @@ func Register(m *macaron.Macaron) {
}) })
// 监控
m.Group("/monitor", func() {
m.Any("/goroutine-num", func(ctx *macaron.Context) string {
return "goroutine数量-" + strconv.Itoa(runtime.NumGoroutine())
})
})
// 任务 // 任务
m.Group("/task", func() { m.Group("/task", func() {
m.Get("/create", task.Create) m.Get("/create", task.Create)
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store) m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
m.Get("", task.Index) m.Get("", task.Index)
m.Get("/log", tasklog.Index) m.Get("/log", tasklog.Index)
m.Post("/log/clear", tasklog.Clear)
m.Post("/remove/:id", task.Remove)
m.Post("/enable/:id", task.Enable)
m.Post("/disable/:id", task.Disable)
}) })
// 主机 // 主机
@ -58,11 +74,52 @@ func Register(m *macaron.Macaron) {
m.Get("/create", host.Create) m.Get("/create", host.Create)
m.Post("/store", binding.Bind(host.HostForm{}), host.Store) m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
m.Get("", host.Index) m.Get("", host.Index)
m.Post("/remove/:id", host.Remove)
}) })
// API接口 // API接口
m.Group("/api/v1", func() { m.Group("/api/v1", func() {
// 添加延时任务
m.Post("/task/delay", delaytask.Create)
})
}
// 中间件注册
func RegisterMiddleware(m *macaron.Macaron) {
m.Use(macaron.Logger())
m.Use(macaron.Recovery())
m.Use(gzip.Gziper())
m.Use(macaron.Static(StaticDir))
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "templates",
Extensions: []string{".html"},
// 模板语法分隔符,默认为 ["{{", "}}"]
Delims: macaron.Delims{"{{{", "}}}"},
// 追加的 Content-Type 头信息,默认为 "UTF-8"
Charset: "UTF-8",
// 渲染具有缩进格式的 JSON默认为不缩进
IndentJSON: true,
// 渲染具有缩进格式的 XML默认为不缩进
IndentXML: true,
}))
m.Use(session.Sessioner())
m.Use(csrf.Csrfer())
m.Use(toolbox.Toolboxer(m))
// 系统未安装,重定向到安装页面
m.Use(func(ctx *macaron.Context) {
installUrl := "/install"
if strings.HasPrefix(ctx.Req.RequestURI, installUrl) {
return
}
if !app.Installed {
ctx.Redirect(installUrl)
}
})
// 设置模板共享变量
m.Use(func(ctx *macaron.Context) {
ctx.Data["URI"] = ctx.Req.RequestURI
ctx.Data["StandardTimeFormat"] = "2006-01-02 15:03:04"
}) })
} }
@ -72,7 +129,6 @@ func isAjaxRequest(ctx *macaron.Context) bool {
return true return true
} }
return false return false
} }

View File

@ -5,7 +5,8 @@ import (
"github.com/ouqiang/gocron/models" "github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils" "github.com/ouqiang/gocron/modules/utils"
"strings" "github.com/ouqiang/gocron/service"
"strconv"
) )
func Index(ctx *macaron.Context) { func Index(ctx *macaron.Context) {
@ -16,7 +17,6 @@ func Index(ctx *macaron.Context) {
} }
ctx.Data["Title"] = "任务列表" ctx.Data["Title"] = "任务列表"
ctx.Data["Tasks"] = tasks ctx.Data["Tasks"] = tasks
ctx.Data["URI"] = "/task"
ctx.HTML(200, "task/index") ctx.HTML(200, "task/index")
} }
@ -25,52 +25,123 @@ func Create(ctx *macaron.Context) {
hosts, err := hostModel.List() hosts, err := hostModel.List()
if err != nil || len(hosts) == 0 { if err != nil || len(hosts) == 0 {
logger.Error(err) logger.Error(err)
ctx.Redirect("/host/create")
} }
logger.Debug(hosts)
ctx.Data["Title"] = "任务管理" ctx.Data["Title"] = "任务管理"
ctx.Data["Hosts"] = hosts ctx.Data["Hosts"] = hosts
ctx.Data["URI"] = "/task/create"
ctx.Data["FirstHostName"] = hosts[0].Name ctx.Data["FirstHostName"] = hosts[0].Name
ctx.Data["FirstHostId"] = hosts[0].Id ctx.Data["FirstHostId"] = hosts[0].Id
ctx.HTML(200, "task/create") ctx.HTML(200, "task/create")
} }
type TaskForm struct { type TaskForm struct {
Name string `binding:"Required"` Name string `binding:"Required;"`
Spec string `binding:"Required"` Spec string `binding:"Required;MaxSize(64)"`
Protocol models.Protocol `binding:"Required"` Protocol models.TaskProtocol `binding:"In(1,2)"`
Type models.TaskType `binding:"Required"` Command string `binding:"Required;MaxSize(512)"`
Command string `binding:"Required"` Timeout int `binding:"Range(0,86400)"`
Timeout int HostId int16
Delay int
HostId int16 `binding:"Required;"`
Remark string Remark string
Status models.Status `binding:"In(1,0)"`
} }
// 保存任务 // 保存任务
func Store(ctx *macaron.Context, form TaskForm) string { func Store(ctx *macaron.Context, form TaskForm) string {
json := utils.Json{} json := utils.JsonResponse{}
taskModel := models.Task{} taskModel := models.Task{}
nameExists, err := taskModel.NameExist(form.Name)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if nameExists {
return json.CommonFailure("任务名称已存在")
}
if form.Protocol == models.TaskSSH && form.HostId <= 0 {
return json.CommonFailure("请选择主机名")
}
taskModel.Name = form.Name taskModel.Name = form.Name
taskModel.Spec = strings.Replace(form.Spec, "\n", "|||", 100)
taskModel.Protocol = form.Protocol taskModel.Protocol = form.Protocol
taskModel.Type = form.Type
taskModel.Command = form.Command taskModel.Command = form.Command
taskModel.Timeout = form.Timeout taskModel.Timeout = form.Timeout
taskModel.HostId = form.HostId taskModel.HostId = form.HostId
taskModel.Delay = form.Delay
taskModel.Remark = form.Remark taskModel.Remark = form.Remark
_, err := taskModel.Create() taskModel.Status = form.Status
taskModel.Spec = form.Spec
insertId, err := taskModel.Create()
if err != nil { if err != nil {
logger.Error(err) return json.CommonFailure("保存失败", err)
return json.Failure(utils.ResponseFailure, "保存失败") }
// 任务处于激活状态,加入调度管理
if (taskModel.Status == models.Enabled) {
addTaskToTimer(insertId)
} }
return json.Success("保存成功", nil) return json.Success("保存成功", nil)
} }
// 删除任务 // 删除任务
func Remove(ctx *macaron.Context) { func Remove(ctx *macaron.Context) string {
id, err := strconv.Atoi(ctx.Params(":id"))
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("参数错误", err)
}
taskModel := new(models.Task)
_, err = taskModel.Delete(id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
service.Cron.RemoveJob(strconv.Itoa(id))
return json.Success(utils.SuccessContent, nil)
}
// 激活任务
func Enable(ctx *macaron.Context) string {
return changeStatus(ctx, models.Enabled)
}
// 暂停任务
func Disable(ctx *macaron.Context) string {
return changeStatus(ctx, models.Disabled)
}
// 改变任务状态
func changeStatus(ctx *macaron.Context, status models.Status) string {
id, err := strconv.Atoi(ctx.Params(":id"))
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("参数错误", err)
}
taskModel := new(models.Task)
_, err = taskModel.Update(id, models.CommonMap{
"Status": status,
})
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if status == models.Enabled {
addTaskToTimer(id)
} else {
service.Cron.RemoveJob(strconv.Itoa(id))
}
return json.Success(utils.SuccessContent, nil)
}
// 添加任务到定时器
func addTaskToTimer(id int) {
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil {
logger.Error(err)
return
}
taskService := service.Task{}
taskService.Add(task)
} }

View File

@ -4,6 +4,7 @@ import (
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models" "github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
) )
// @author qiang.ou<qingqianludao@gmail.com> // @author qiang.ou<qingqianludao@gmail.com>
@ -17,6 +18,17 @@ func Index(ctx *macaron.Context) {
} }
ctx.Data["Title"] = "任务日志" ctx.Data["Title"] = "任务日志"
ctx.Data["Logs"] = logs ctx.Data["Logs"] = logs
ctx.Data["URI"] = "/task/log"
ctx.HTML(200, "task/log") ctx.HTML(200, "task/log")
}
// 清空日志
func Clear(ctx *macaron.Context) string {
taskLogModel := new(models.TaskLog)
_, err := taskLogModel.Clear()
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure(utils.FailureContent)
}
return json.Success(utils.SuccessContent, nil)
} }

View File

@ -2,20 +2,23 @@ package service
import ( import (
"github.com/ouqiang/gocron/models" "github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/crontask"
"github.com/robfig/cron"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/ssh" "github.com/ouqiang/gocron/modules/ssh"
"github.com/jakecoffman/cron"
) )
var Cron *cron.Cron
type Task struct{} type Task struct{}
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行 // 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
func (task *Task) Initialize() { func (task *Task) Initialize() {
Cron = cron.New()
Cron.Start()
taskModel := new(models.Task) taskModel := new(models.Task)
taskList, err := taskModel.ActiveList() taskList, err := taskModel.ActiveList()
if err != nil { if err != nil {
@ -38,17 +41,10 @@ func (task *Task) Add(taskModel models.TaskHost) {
logger.Error("添加任务#不存在的任务协议编号", taskModel.Protocol) logger.Error("添加任务#不存在的任务协议编号", taskModel.Protocol)
return return
} }
// 定时任务
if taskModel.Type == models.Timing { cronName := strconv.Itoa(taskModel.Id)
err := crontask.DefaultCronTask.Update(strconv.Itoa(taskModel.Id), taskModel.Spec, taskFunc) Cron.RemoveJob(cronName)
if err != nil { Cron.AddFunc(taskModel.Spec, taskFunc, cronName)
logger.Error(err)
}
} else if taskModel.Type == models.Delay {
// 延时任务
delay := time.Duration(taskModel.Delay) * time.Second
time.AfterFunc(delay, taskFunc)
}
} }
type Handler interface { type Handler interface {
@ -104,15 +100,13 @@ func (h *SSHCommandHandler) Run(taskModel models.TaskHost) (string, error) {
} }
func createTaskLog(taskModel models.TaskHost) (int, error) { func createTaskLog(taskModel models.TaskHost) (int64, error) {
taskLogModel := new(models.TaskLog) taskLogModel := new(models.TaskLog)
taskLogModel.Name = taskModel.Task.Name taskLogModel.Name = taskModel.Task.Name
taskLogModel.Spec = taskModel.Spec taskLogModel.Spec = taskModel.Spec
taskLogModel.Protocol = taskModel.Protocol taskLogModel.Protocol = taskModel.Protocol
taskLogModel.Type = taskModel.Type
taskLogModel.Command = taskModel.Command taskLogModel.Command = taskModel.Command
taskLogModel.Timeout = taskModel.Timeout taskLogModel.Timeout = taskModel.Timeout
taskLogModel.Delay = taskModel.Delay
taskLogModel.Hostname = taskModel.Name taskLogModel.Hostname = taskModel.Name
taskLogModel.StartTime = time.Now() taskLogModel.StartTime = time.Now()
taskLogModel.Status = models.Running taskLogModel.Status = models.Running
@ -121,7 +115,7 @@ func createTaskLog(taskModel models.TaskHost) (int, error) {
return insertId, err return insertId, err
} }
func updateTaskLog(taskLogId int, result string, err error) (int64, error) { func updateTaskLog(taskLogId int64, result string, err error) (int64, error) {
taskLogModel := new(models.TaskLog) taskLogModel := new(models.TaskLog)
var status models.Status var status models.Status
if err != nil { if err != nil {
@ -140,9 +134,9 @@ func updateTaskLog(taskLogId int, result string, err error) (int64, error) {
func createHandlerJob(taskModel models.TaskHost) cron.FuncJob { func createHandlerJob(taskModel models.TaskHost) cron.FuncJob {
var handler Handler = nil var handler Handler = nil
switch taskModel.Protocol { switch taskModel.Protocol {
case models.HTTP: case models.TaskHTTP:
handler = new(HTTPHandler) handler = new(HTTPHandler)
case models.SSHCommand: case models.TaskSSH:
handler = new(SSHCommandHandler) handler = new(SSHCommandHandler)
} }
if handler == nil { if handler == nil {
@ -162,10 +156,10 @@ func createHandlerJob(taskModel models.TaskHost) cron.FuncJob {
if err == nil { if err == nil {
break break
} else { } else {
logger.Error("执行失败#tasklog.id-" + strconv.Itoa(taskLogId) + "#尝试次数-" + strconv.Itoa(i + 1) + "#" + err.Error() + " " + result) logger.Error("执行失败#tasklog.id-" + strconv.FormatInt(taskLogId, 10) + "#尝试次数-" + strconv.Itoa(i + 1) + "#" + err.Error() + " " + result)
} }
} }
_, err = updateTaskLog(int(taskLogId), result, err) _, err = updateTaskLog(taskLogId, result, err)
if err != nil { if err != nil {
logger.Error("更新任务日志失败-", err) logger.Error("更新任务日志失败-", err)
} }

View File

@ -1,12 +1,10 @@
</div>
</div> </div>
<footer> <footer>
<div id="copyrights"> <div id="copyrights">
<div class="inset"> <div class="inset">
<div class="bigcontainer"> <div class="bigcontainer">
<div class="fl"> <div class="fl">
<p>&copy; 2017 cron-scheduler <p>&copy; 2017 gocron
<i class="Github Alternate icon"></i><a href="https://github.com/ouqiang/cron-scheduler" target="_blank"> <i class="Github Alternate icon"></i><a href="https://github.com/ouqiang/cron-scheduler" target="_blank">
GitHub GitHub
</a> </a>

View File

@ -12,6 +12,7 @@
<script type="text/javascript" src="/resource/javascript/framework.js"></script> <script type="text/javascript" src="/resource/javascript/framework.js"></script>
<script type="text/javascript" src="/resource/javascript/form.min.js"></script> <script type="text/javascript" src="/resource/javascript/form.min.js"></script>
<script type="text/javascript" src="/resource/sweetalert/sweetalert.min.js"></script> <script type="text/javascript" src="/resource/sweetalert/sweetalert.min.js"></script>
<script type="text/javascript" src="/resource/javascript/vue.min.js"></script>
<script type="text/javascript" src="/resource/javascript/main.js"></script> <script type="text/javascript" src="/resource/javascript/main.js"></script>
</head> </head>
<body> <body>
@ -47,11 +48,11 @@
<div class="ui teal inverted menu"> <div class="ui teal inverted menu">
<div class="bigcontainer"> <div class="bigcontainer">
<div class="right menu"> <div class="right menu">
<a class="item" href="/"><i class="home icon"></i>首页</a> <a class="item {{{if eq .URI "/"}}}active{{{end}}}" href="/"><i class="home icon"></i>首页</a>
<a class="item" href="/task"><i class="tasks icon"></i>任务</a> <a class="item {{{if eq .URI "/task"}}}active{{{end}}}" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item" href="/host"><i class="linux icon"></i>主机</a> <a class="item {{{if eq .URI "/host"}}}active{{{end}}}" href="/host"><i class="linux icon"></i>主机</a>
<a class="active item" href="/user"><i class="user icon"></i>账户</a> <a class="item {{{if eq .URI "/user"}}}active{{{end}}}" href="/user"><i class="user icon"></i>账户</a>
<a class="item {{{if eq .URI "/admin"}}}active{{{end}}}" href="/admin"><i class="settings icon"></i>管理</a>
</div> </div>
</div> </div>
</div> </div>
<div class="container">

View File

@ -1,5 +1,12 @@
{{{ template "common/header" . }}} {{{ template "common/header" . }}}
<script>
404错误 swal({
title: "404 - NOT FOUND",
text: "您访问的页面不存在",
type: "warning"
},
function(){
location.href = "/"
});
</script>
{{{ template "common/footer" . }}} {{{ template "common/footer" . }}}

View File

@ -1,5 +1,11 @@
{{{ template "common/header" . }}} {{{ template "common/header" . }}}
<script>
500错误 swal({
title: "500 - INTERNAL SERVER ERROR",
text: "网站暂时无法访问, 请稍后再试.",
type: "warning",
confirmButtonColor: "#DD6B55",
confirmButtonText: "确定"
});
</script>
{{{ template "common/footer" . }}} {{{ template "common/footer" . }}}

View File

@ -0,0 +1,3 @@
{{{ template "common/header" . }}}
{{{ template "common/footer" . }}}

View File

@ -73,7 +73,6 @@
$('.ui.form').form( $('.ui.form').form(
{ {
onSuccess: function(event, fields) { onSuccess: function(event, fields) {
var util = new Util();
util.post('/host/store', fields, function(code, message) { util.post('/host/store', fields, function(code, message) {
location.href = "/host" location.href = "/host"
}); });

View File

@ -23,6 +23,7 @@
<th>密码</th> <th>密码</th>
<th>端口</th> <th>端口</th>
<th>备注</th> <th>备注</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -34,6 +35,7 @@
<td>{{{.Password}}}</td> <td>{{{.Password}}}</td>
<td>{{{.Port}}}</td> <td>{{{.Port}}}</td>
<td>{{{.Remark}}}</td> <td>{{{.Remark}}}</td>
<td><button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button></td>
</tr> </tr>
{{{end}}} {{{end}}}
</tbody> </tbody>

View File

@ -85,7 +85,6 @@
$('.ui.form').form( $('.ui.form').form(
{ {
onSuccess: function(event, fields) { onSuccess: function(event, fields) {
var util = new Util();
util.post('/install/store', fields, function(code, message) { util.post('/install/store', fields, function(code, message) {
swal('安装成功'); swal('安装成功');
location.href = "/"; location.href = "/";

View File

@ -1,8 +1,6 @@
{{{ template "common/header" . }}} {{{ template "common/header" . }}}
<div class="ui grid"> <div class="ui grid">
<!--the vertical menu-->
{{{template "task/menu" .}}} {{{template "task/menu" .}}}
<div class="twelve wide column"> <div class="twelve wide column">
<div class="pageHeader"> <div class="pageHeader">
@ -38,21 +36,8 @@
<div class="default text">SSH-Command (执行shell命令)</div> <div class="default text">SSH-Command (执行shell命令)</div>
<i class="dropdown icon"></i> <i class="dropdown icon"></i>
<div class="menu"> <div class="menu">
<div class="item active" data-value="2">SSH-Command (执行shell命令)</div> <div class="item active" data-value="2">SSH</div>
<div class="item" data-value="1">HTTP (执行http-post请求)</div> <div class="item" data-value="1">HTTP</div>
</div>
</div>
</div>
<div class="field">
<label>任务类型</label>
<div class="ui dropdown selection">
<input type="hidden" name="type" value="1">
<div class="default text">定时任务</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item active" data-value="1">定时任务</div>
<div class="item" data-value="2">延时任务</div>
</div> </div>
</div> </div>
</div> </div>
@ -71,29 +56,38 @@
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label>命令 (根据选择的协议类型确定,shell命令|URL地址)</label> <label>命令(shell命令|URL地址, 多条shell命令";"分隔)</label>
<input type="text" name="command" placeholder="tail -n 10 /var/log/nginx/error.log"> <input type="text" name="command" placeholder="tail -n 10 /var/log/nginx/error.log">
</div> </div>
<div class="three fields"> <div class="three fields">
<div class="field"> <div class="field">
<label>任务超时时间 (单位秒,0不限制,不能超过24小时, 默认0)</label> <label>任务超时时间 (单位秒,0不限制,不能超过24小时)</label>
<input type="text" name="timeout" placeholder="60"> <input type="text" name="timeout" placeholder="0" value="180">
</div>
<div class="field">
<label>延时时间 (单位秒)</label>
<input type="text" name="delay" placeholder="60">
</div> </div>
</div> </div>
<div class="three field"> <div class="three fields">
<div class="field">
<label>任务状态 (任务添加成功后,是否立即调度)</label>
<div class="ui dropdown selection">
<input type="hidden" name="status" value="0">
<div class="default text">激活</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item active" data-value="0">暂停</div>
<div class="item" data-value="1">激活</div>
</div>
</div>
</div>
</div>
<div class="two field">
<div class="field"> <div class="field">
<label>备注</label> <label>备注</label>
<textarea rows="5" name="remark" placeholder="数据库备份, 每天凌晨执行一次"></textarea> <textarea rows="5" name="remark" placeholder="任务备注"></textarea>
</div> </div>
</div> </div>
<div class="ui primary submit button">提交</div> <div class="ui primary submit button">提交</div>
</form> </form>
</div> </div>
<!--the newDevice form-->
</div> </div>
@ -104,7 +98,6 @@
$('.ui.form').form( $('.ui.form').form(
{ {
onSuccess: function(event, fields) { onSuccess: function(event, fields) {
var util = new Util();
util.post('/task/store', fields, function(code, message) { util.post('/task/store', fields, function(code, message) {
location.href = "/task" location.href = "/task"
}); });

View File

@ -1,8 +1,6 @@
{{{ template "common/header" . }}} {{{ template "common/header" . }}}
<div class="ui grid"> <div class="ui grid">
<!--the vertical menu-->
{{{template "task/menu" .}}} {{{template "task/menu" .}}}
<div class="twelve wide column"> <div class="twelve wide column">
<div class="pageHeader"> <div class="pageHeader">
@ -15,45 +13,65 @@
</h3> </h3>
</div> </div>
</div> </div>
<table class="ui single line table"> <table class="ui violet table">
<thead> <thead>
<tr> <tr>
<th>任务名称</th> <th>任务名称</th>
<th>cron表达式</th> <th>cron表达式</th>
<th>协议</th> <th>协议</th>
<th>任务类型</th> <th width="5%">命令</th>
<th>命令</th>
<th>超时时间(秒)</th> <th>超时时间(秒)</th>
<th>延迟时间(秒)</th>
<th>主机</th> <th>主机</th>
<th>备注</th> <th>备注</th>
<th>状态</th> <th>状态</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{{range $i, $v := .Tasks}}} {{{range $i, $v := .Tasks}}}
<tr> <tr>
<td>{{{.Name}}}</td> <td>{{{.Task.Name}}}</td>
<td>{{{.Spec}}}</td> <td>{{{.Spec}}}</td>
<td>{{{.Protocol}}}</td> <td>{{{if eq .Protocol 1}}} HTTP {{{else}}} SSH {{{end}}}</td>
<td>{{{.Type}}}</td>
<td>{{{.Command}}}</td> <td>{{{.Command}}}</td>
<td>{{{.Timeout}}}</td> <td>{{{.Timeout}}}</td>
<td>{{{.Delay}}}</td> <td>{{{.Alias}}}</td>
<td>{{{.Hostname}}}</td>
<td>{{{.Remark}}}</td> <td>{{{.Remark}}}</td>
<td>{{{.Status}}}</td> <td>{{{if eq .Status 1}}}<i class="large checkmark blue icon"></i> {{{else}}} <i class="large red minus icon"></i> {{{end}}}</td>
<td>
{{{if eq .Status 1}}}
<button class="ui primary button" onclick="changeStatus({{{.Id}}},{{{.Status}}})">暂停</button>
{{{else}}}
<button class="ui blue button" onclick="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
{{{end}}}
<button class="ui positive button" onclick="util.removeConfirm('/task/remove/{{{.Id}}}')">删除</button>
<button class="ui pink button">查看日志</button>
</td>
</tr> </tr>
{{{end}}} {{{end}}}
</tbody> </tbody>
</table> </table>
</div> </div>
<!--the newDevice form-->
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$('.ui.checkbox').checkbox(); $('.ui.checkbox').checkbox();
function changeStatus(id ,status) {
var url = '';
if (status) {
url = '/task/disable';
} else {
url = '/task/enable';
}
url += '/' + id;
util.post(url,{}, function() {
location.reload();
});
}
</script> </script>
{{{ template "common/footer" . }}} {{{ template "common/footer" . }}}

View File

@ -1,5 +1,13 @@
{{{ template "common/header" . }}} {{{ template "common/header" . }}}
<style type="text/css">
pre {
white-space: pre-wrap;
word-wrap: break-word;
padding:10px;
background-color: #4C4C4C;
color: white;
}
</style>
<div class="ui grid"> <div class="ui grid">
<!--the vertical menu--> <!--the vertical menu-->
{{{ template "task/menu" . }}} {{{ template "task/menu" . }}}
@ -8,28 +16,25 @@
<div class="pageHeader"> <div class="pageHeader">
<div class="segment"> <div class="segment">
<h3 class="ui dividing header"> <h3 class="ui dividing header">
<i class="large add icon"></i>
<div class="content"> <div class="content">
任务日志 <button class="ui small teal button" onclick="clearLog()">清空日志</button>
</div> </div>
</h3> </h3>
</div> </div>
</div> </div>
<table class="ui single line table"> <table class="ui pink table">
<thead> <thead>
<tr> <tr>
<th width="8%">任务名称</th> <th>任务名称</th>
<th width="8%">cron表达式</th> <th>cron表达式</th>
<th width="8%">协议</th> <th>协议</th>
<th width="8%">任务类型</th> <th>超时时间(秒)</th>
<th width="8%">命令</th> <th>主机</th>
<th width="8%">超时时间(秒)</th> <th>开始时间</th>
<th width="8%">延迟时间(秒)</th> <th>结束时间</th>
<th width="8%">主机</th> <th>状态</th>
<th width="8%">开始时间</th> <th>执行结果</th>
<th width="8%">结束时间</th>
<th width="8%">状态</th>
<th width="8%">执行结果</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -37,30 +42,89 @@
<tr> <tr>
<td>{{{.Name}}}</td> <td>{{{.Name}}}</td>
<td>{{{.Spec}}}</td> <td>{{{.Spec}}}</td>
<td>{{{.Protocol}}}</td> <td>{{{if eq .Protocol 1}}} HTTP {{{else}}} SSH {{{end}}}</td>
<td>{{{.Type}}}</td>
<td>{{{.Command}}}</td>
<td>{{{.Timeout}}}</td> <td>{{{.Timeout}}}</td>
<td>{{{.Delay}}}</td> <td>{{{.Hostname}}}</td>
<td>{{{.SshHosts}}}</td> <td>
<td>{{{.StartTime}}}</td> {{{.StartTime.Format "2006-01-02 15:03:04" }}}
<td>{{{.EndTime}}}</td> </td>
<td>{{{.Status}}}</td> <td>
<td>{{{.Result}}}</td> {{{.EndTime.Format "2006-01-02 15:03:04" }}}
</td>
<td>
{{{if eq .Status 2}}}
成功
{{{else if eq .Status 1}}}
<span style="color:green">执行中</span>
{{{else if eq .Status 0}}}
<span style="color:red">失败</span>
{{{else}}}
<span style="color:#4499EE">待执行</span>
{{{end}}}
</td>
<td>
<button class="ui small primary button"
onclick="showResult('{{{.Name}}}', '{{{.Command}}}', '{{{.Result}}}')"
>查看结果
</button>
</td>
</tr> </tr>
{{{end}}} {{{end}}}
</tbody> </tbody>
</table> </table>
</div> </div>
<!--the newDevice form-->
</div> </div>
<div class="message">
<result></result>
</div>
<script type="text/x-vue-template" id="task-result">
<div class="ui modal">
<i class="close icon"></i>
<div class="header">
{{name}}
</div>
<div>
<pre style="background-color:#04477C;color:lightslategray">{{command}}</pre>
</div>
<div>
<pre>{{result}}</pre>
</div>
</div>
</script>
<script type="text/javascript"> <script type="text/javascript">
function showResult(name, command,result) {
$('.message').html($('#task-result').html());
new Vue(
{
el: '.message',
data: {
result: result.replace(/\\n/,"<br>"),
name: name,
command: command
}
}
);
$('.ui.modal.transition').remove();
$('.ui.modal').modal({
detachable: false,
observeChanges: true
}).modal('refresh').modal('show');
}
function clearLog() {
util.confirm("确定要删除所有日志吗?", function() {
util.post("/task/log/clear",{}, function() {
location.reload();
});
});
}
$('.ui.form').form( $('.ui.form').form(
{ {
onSuccess: function(event, fields) { onSuccess: function(event, fields) {
var util = new Util();
util.post('/host/store', fields, function(code, message) { util.post('/host/store', fields, function(code, message) {
location.reload(); location.reload();
}); });
@ -108,5 +172,4 @@
inline : true inline : true
}); });
</script> </script>
{{{ template "common/footer" . }}} {{{ template "common/footer" . }}}

141
vendor/github.com/jakecoffman/cron/README.md generated vendored Normal file
View File

@ -0,0 +1,141 @@
cron
====
A cron library for Go. See the
[godoc](http://go.pkgdoc.org/github.com/robfig/cron).
## Usage
Callers may register Funcs to be invoked on a given schedule. Cron will run
them in their own goroutines. A name must be provided.
```go
c := cron.New()
c.AddFunc("0 5 * * * *", func() { fmt.Println("Every 5 minutes") }, "Often")
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }, "Frequent")
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }, "Less Frequent")
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") }, "My Job")
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
// Remove an entry from the cron by name.
c.RemoveJob("My Job")
..
c.Stop() // Stop the scheduler (does not stop any jobs already running).
```
## CRON Expression
This section describes the specific format accepted by this cron. Some snippets
are taken from [the wikipedia article](http://en.wikipedia.org/wiki/Cron).
### Format
A cron expression represents a set of times, using 6 space-separated fields.
Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
and "sun" are equally accepted.
### Special Characters
#### Asterisk ( * )
The asterisk indicates that the cron expression will match for all values of the
field; e.g., using an asterisk in the 5th field (month) would indicate every
month.
#### Slash ( / )
Slashes are used to describe increments of ranges. For example 3-59/15 in the
1st field (minutes) would indicate the 3rd minute of the hour and every 15
minutes thereafter. The form "*/..." is equivalent to the form "first-last/...",
that is, an increment over the largest possible range of the field. The form
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
increment until the end of that specific range. It does not wrap around.
#### Comma ( , )
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
#### Hyphen ( - )
Hyphens are used to define ranges. For example, 9-17 would indicate every
hour between 9am and 5pm inclusive.
#### Question mark ( ? )
Question mark may be used instead of '*' for leaving either day-of-month or
day-of-week blank.
### Predefined schedules
You may use one of several pre-defined schedules in place of a cron expression.
Entry | Description | Equivalent To
----- | ----------- | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | <code>0 0 0 1 1 *</code>
@monthly | Run once a month, midnight, first of month | <code>0 0 0 1 * *</code>
@weekly | Run once a week, midnight on Sunday | <code>0 0 0 * * 0</code>
@daily (or @midnight) | Run once a day, midnight | <code>0 0 0 * * *</code>
@hourly | Run once an hour, beginning of hour | <code>0 0 * * * *</code>
## Intervals
You may also schedule a job to execute at fixed intervals. This is supported by
formatting the cron spec like this:
@every <duration>
where `<duration>` is a string accepted by
[`time.ParseDuration`](http://golang.org/pkg/time/#ParseDuration).
For example, `@every 1h30m10s` would indicate a schedule that activates every
1 hour, 30 minutes, 10 seconds.
> Note: The interval does not take the job runtime into account. For example,
> if a job takes *3 minutes* to run, and it is scheduled to run every *5 minutes*,
> it will have only *2 minutes* of idle time between each run.
## Time zones
All interpretation and scheduling is done in the machine's local time zone (as
provided by the [Go time package](http://www.golang.org/pkg/time)).
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
not be run!
## Thread safety
Since the Cron service runs concurrently with the calling code, some amount of
care must be taken to ensure proper synchronization.
All [cron methods](http://go.pkgdoc.org/github.com/robfig/cron#Cron) are
designed to be correctly synchronized as long as the caller ensures that
invocations have a clear happens-before ordering between them.
## Implementation
Cron entries are stored in an array, sorted by their next activation time. Cron
sleeps until the next job is due to be run.
Upon waking:
* it runs each entry that is active on that second
* it calculates the next run times for the jobs that were run
* it re-sorts the array of entries by next activation time.
* it goes to sleep until the soonest job.

View File

@ -9,11 +9,12 @@ type ConstantDelaySchedule struct {
} }
// Every returns a crontab Schedule that activates once every duration. // Every returns a crontab Schedule that activates once every duration.
// Delays of less than a second are not supported (will round up to 1 second). // Delays of less than a second are not supported (will panic).
// Any fields less than a Second are truncated. // Any fields less than a Second are truncated.
func Every(duration time.Duration) ConstantDelaySchedule { func Every(duration time.Duration) ConstantDelaySchedule {
if duration < time.Second { if duration < time.Second {
duration = time.Second panic("cron/constantdelay: delays of less than a second are not supported: " +
duration.String())
} }
return ConstantDelaySchedule{ return ConstantDelaySchedule{
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,

View File

@ -3,23 +3,22 @@
package cron package cron
import ( import (
"log"
"runtime"
"sort" "sort"
"time" "time"
) )
type entries []*Entry
// Cron keeps track of any number of entries, invoking the associated func as // Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may be started, stopped, and the entries may // specified by the schedule. It may be started, stopped, and the entries may
// be inspected while running. // be inspected while running.
type Cron struct { type Cron struct {
entries []*Entry entries entries
stop chan struct{} stop chan struct{}
add chan *Entry add chan *Entry
snapshot chan []*Entry remove chan string
snapshot chan entries
running bool running bool
ErrorLog *log.Logger
location *time.Location
} }
// Job is an interface for submitted cron jobs. // Job is an interface for submitted cron jobs.
@ -49,6 +48,9 @@ type Entry struct {
// The Job to run. // The Job to run.
Job Job Job Job
// Unique name to identify the Entry so as to be able to remove it later.
Name string
} }
// byTime is a wrapper for sorting the entry array by time // byTime is a wrapper for sorting the entry array by time
@ -70,21 +72,15 @@ func (s byTime) Less(i, j int) bool {
return s[i].Next.Before(s[j].Next) return s[i].Next.Before(s[j].Next)
} }
// New returns a new Cron job runner, in the Local time zone. // New returns a new Cron job runner.
func New() *Cron { func New() *Cron {
return NewWithLocation(time.Now().Location())
}
// NewWithLocation returns a new Cron job runner.
func NewWithLocation(location *time.Location) *Cron {
return &Cron{ return &Cron{
entries: nil, entries: nil,
add: make(chan *Entry), add: make(chan *Entry),
remove: make(chan string),
stop: make(chan struct{}), stop: make(chan struct{}),
snapshot: make(chan []*Entry), snapshot: make(chan entries),
running: false, running: false,
ErrorLog: nil,
location: location,
} }
} }
@ -94,27 +90,53 @@ type FuncJob func()
func (f FuncJob) Run() { f() } func (f FuncJob) Run() { f() }
// AddFunc adds a func to the Cron to be run on the given schedule. // AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error { func (c *Cron) AddFunc(spec string, cmd func(), name string) {
return c.AddJob(spec, FuncJob(cmd)) c.AddJob(spec, FuncJob(cmd), name)
} }
// AddJob adds a Job to the Cron to be run on the given schedule. // AddFunc adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error { func (c *Cron) AddJob(spec string, cmd Job, name string) {
schedule, err := Parse(spec) c.Schedule(Parse(spec), cmd, name)
if err != nil { }
return err
// RemoveJob removes a Job from the Cron based on name.
func (c *Cron) RemoveJob(name string) {
if !c.running {
i := c.entries.pos(name)
if i == -1 {
return
}
c.entries = c.entries[:i+copy(c.entries[i:], c.entries[i+1:])]
return
} }
c.Schedule(schedule, cmd)
return nil c.remove <- name
}
func (entrySlice entries) pos(name string) int {
for p, e := range entrySlice {
if e.Name == name {
return p
}
}
return -1
} }
// Schedule adds a Job to the Cron to be run on the given schedule. // Schedule adds a Job to the Cron to be run on the given schedule.
func (c *Cron) Schedule(schedule Schedule, cmd Job) { func (c *Cron) Schedule(schedule Schedule, cmd Job, name string) {
entry := &Entry{ entry := &Entry{
Schedule: schedule, Schedule: schedule,
Job: cmd, Job: cmd,
Name: name,
} }
if !c.running { if !c.running {
i := c.entries.pos(entry.Name)
if i != -1 {
return
}
c.entries = append(c.entries, entry) c.entries = append(c.entries, entry)
return return
} }
@ -132,37 +154,17 @@ func (c *Cron) Entries() []*Entry {
return c.entrySnapshot() return c.entrySnapshot()
} }
// Location gets the time zone location // Start the cron scheduler in its own go-routine.
func (c *Cron) Location() *time.Location {
return c.location
}
// Start the cron scheduler in its own go-routine, or no-op if already started.
func (c *Cron) Start() { func (c *Cron) Start() {
if c.running {
return
}
c.running = true c.running = true
go c.run() go c.run()
} }
func (c *Cron) runWithRecovery(j Job) {
defer func() {
if r := recover(); r != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.logf("cron: panic running job: %v\n%s", r, buf)
}
}()
j.Run()
}
// Run the scheduler.. this is private just due to the need to synchronize // Run the scheduler.. this is private just due to the need to synchronize
// access to the 'running' state variable. // access to the 'running' state variable.
func (c *Cron) run() { func (c *Cron) run() {
// Figure out the next activation times for each entry. // Figure out the next activation times for each entry.
now := time.Now().In(c.location) now := time.Now().Local()
for _, entry := range c.entries { for _, entry := range c.entries {
entry.Next = entry.Schedule.Next(now) entry.Next = entry.Schedule.Next(now)
} }
@ -180,53 +182,50 @@ func (c *Cron) run() {
effective = c.entries[0].Next effective = c.entries[0].Next
} }
timer := time.NewTimer(effective.Sub(now))
select { select {
case now = <-timer.C: case now = <-time.After(effective.Sub(now)):
now = now.In(c.location)
// Run every entry whose next time was this effective time. // Run every entry whose next time was this effective time.
for _, e := range c.entries { for _, e := range c.entries {
if e.Next != effective { if e.Next != effective {
break break
} }
go c.runWithRecovery(e.Job) go e.Job.Run()
e.Prev = e.Next e.Prev = e.Next
e.Next = e.Schedule.Next(now) e.Next = e.Schedule.Next(effective)
} }
continue continue
case newEntry := <-c.add: case newEntry := <-c.add:
i := c.entries.pos(newEntry.Name)
if i != -1 {
break
}
c.entries = append(c.entries, newEntry) c.entries = append(c.entries, newEntry)
newEntry.Next = newEntry.Schedule.Next(time.Now().In(c.location)) newEntry.Next = newEntry.Schedule.Next(time.Now().Local())
case name := <-c.remove:
i := c.entries.pos(name)
if i == -1 {
break
}
c.entries = c.entries[:i+copy(c.entries[i:], c.entries[i+1:])]
case <-c.snapshot: case <-c.snapshot:
c.snapshot <- c.entrySnapshot() c.snapshot <- c.entrySnapshot()
case <-c.stop: case <-c.stop:
timer.Stop()
return return
} }
// 'now' should be updated after newEntry and snapshot cases. // 'now' should be updated after newEntry and snapshot cases.
now = time.Now().In(c.location) now = time.Now().Local()
timer.Stop()
} }
} }
// Logs an error to stderr or to the configured error log // Stop the cron scheduler.
func (c *Cron) logf(format string, args ...interface{}) {
if c.ErrorLog != nil {
c.ErrorLog.Printf(format, args...)
} else {
log.Printf(format, args...)
}
}
// Stop stops the cron scheduler if it is running; otherwise it does nothing.
func (c *Cron) Stop() { func (c *Cron) Stop() {
if !c.running {
return
}
c.stop <- struct{}{} c.stop <- struct{}{}
c.running = false c.running = false
} }

223
vendor/github.com/jakecoffman/cron/parser.go generated vendored Normal file
View File

@ -0,0 +1,223 @@
package cron
import (
"log"
"math"
"strconv"
"strings"
"time"
)
// Parse returns a new crontab schedule representing the given spec.
// It panics with a descriptive error if the spec is not valid.
//
// It accepts
// - Full crontab specs, e.g. "* * * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func Parse(spec string) Schedule {
if spec[0] == '@' {
return parseDescriptor(spec)
}
// Split on whitespace. We require 5 or 6 fields.
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
fields := strings.Fields(spec)
if len(fields) != 5 && len(fields) != 6 {
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
}
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
if len(fields) == 5 {
fields = append(fields, "*")
}
schedule := &SpecSchedule{
Second: getField(fields[0], seconds),
Minute: getField(fields[1], minutes),
Hour: getField(fields[2], hours),
Dom: getField(fields[3], dom),
Month: getField(fields[4], months),
Dow: getField(fields[5], dow),
}
return schedule
}
// getField returns an Int with the bits set representing all of the times that
// the field represents. A "field" is a comma-separated list of "ranges".
func getField(field string, r bounds) uint64 {
// list = range {"," range}
var bits uint64
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bits |= getRange(expr, r)
}
return bits
}
// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
func getRange(expr string, r bounds) uint64 {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
)
var extra_star uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra_star = starBit
} else {
start = parseIntOrName(lowAndHigh[0], r.names)
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end = parseIntOrName(lowAndHigh[1], r.names)
default:
log.Panicf("Too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step = mustParseInt(rangeAndStep[1])
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
log.Panicf("Too many slashes: %s", expr)
}
if start < r.min {
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
return getBits(start, end, step) | extra_star
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) uint {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt
}
}
return mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or panics.
func mustParseInt(expr string) uint {
num, err := strconv.Atoi(expr)
if err != nil {
log.Panicf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
}
return uint(num)
}
// getBits sets all bits in the range [min, max], modulo the given step size.
func getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// all returns all bits within the given bounds. (plus the star bit)
func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}
// parseDescriptor returns a pre-defined schedule for the expression, or panics
// if none matches.
func parseDescriptor(spec string) Schedule {
switch spec {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: 1 << months.min,
Dow: all(dow),
}
case "@monthly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: all(months),
Dow: all(dow),
}
case "@weekly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: 1 << dow.min,
}
case "@daily", "@midnight":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
case "@hourly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: all(hours),
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
}
const every = "@every "
if strings.HasPrefix(spec, every) {
duration, err := time.ParseDuration(spec[len(every):])
if err != nil {
log.Panicf("Failed to parse duration %s: %s", spec, err)
}
return Every(duration)
}
log.Panicf("Unrecognized descriptor: %s", spec)
return nil
}

View File

@ -1,6 +1,8 @@
package cron package cron
import "time" import (
"time"
)
// SpecSchedule specifies a duty cycle (to the second granularity), based on a // SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets. // traditional crontab specification. It is computed initially and stored as bit sets.
@ -120,7 +122,7 @@ WRAP:
for 1<<uint(t.Minute())&s.Minute == 0 { for 1<<uint(t.Minute())&s.Minute == 0 {
if !added { if !added {
added = true added = true
t = t.Truncate(time.Minute) t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
} }
t = t.Add(1 * time.Minute) t = t.Add(1 * time.Minute)
@ -132,7 +134,7 @@ WRAP:
for 1<<uint(t.Second())&s.Second == 0 { for 1<<uint(t.Second())&s.Second == 0 {
if !added { if !added {
added = true added = true
t = t.Truncate(time.Second) t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location())
} }
t = t.Add(1 * time.Second) t = t.Add(1 * time.Second)
@ -151,6 +153,7 @@ func dayMatches(s *SpecSchedule, t time.Time) bool {
domMatch bool = 1<<uint(t.Day())&s.Dom > 0 domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0 dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
) )
if s.Dom&starBit > 0 || s.Dow&starBit > 0 { if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch return domMatch && dowMatch
} }

View File

@ -1,2 +0,0 @@
[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron)
[![Build Status](https://travis-ci.org/robfig/cron.svg?branch=master)](https://travis-ci.org/robfig/cron)

129
vendor/github.com/robfig/cron/doc.go generated vendored
View File

@ -1,129 +0,0 @@
/*
Package cron implements a cron spec parser and job runner.
Usage
Callers may register Funcs to be invoked on a given schedule. Cron will run
them in their own goroutines.
c := cron.New()
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("@hourly", func() { fmt.Println("Every hour") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") })
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
c.Stop() // Stop the scheduler (does not stop any jobs already running).
CRON Expression Format
A cron expression represents a set of times, using 6 space-separated fields.
Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
and "sun" are equally accepted.
Special Characters
Asterisk ( * )
The asterisk indicates that the cron expression will match for all values of the
field; e.g., using an asterisk in the 5th field (month) would indicate every
month.
Slash ( / )
Slashes are used to describe increments of ranges. For example 3-59/15 in the
1st field (minutes) would indicate the 3rd minute of the hour and every 15
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...",
that is, an increment over the largest possible range of the field. The form
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
increment until the end of that specific range. It does not wrap around.
Comma ( , )
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
Hyphen ( - )
Hyphens are used to define ranges. For example, 9-17 would indicate every
hour between 9am and 5pm inclusive.
Question mark ( ? )
Question mark may be used instead of '*' for leaving either day-of-month or
day-of-week blank.
Predefined schedules
You may use one of several pre-defined schedules in place of a cron expression.
Entry | Description | Equivalent To
----- | ----------- | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 *
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * *
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
@hourly | Run once an hour, beginning of hour | 0 0 * * * *
Intervals
You may also schedule a job to execute at fixed intervals. This is supported by
formatting the cron spec like this:
@every <duration>
where "duration" is a string accepted by time.ParseDuration
(http://golang.org/pkg/time/#ParseDuration).
For example, "@every 1h30m10s" would indicate a schedule that activates every
1 hour, 30 minutes, 10 seconds.
Note: The interval does not take the job runtime into account. For example,
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,
it will have only 2 minutes of idle time between each run.
Time zones
All interpretation and scheduling is done in the machine's local time zone (as
provided by the Go time package (http://www.golang.org/pkg/time).
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
not be run!
Thread safety
Since the Cron service runs concurrently with the calling code, some amount of
care must be taken to ensure proper synchronization.
All cron methods are designed to be correctly synchronized as long as the caller
ensures that invocations have a clear happens-before ordering between them.
Implementation
Cron entries are stored in an array, sorted by their next activation time. Cron
sleeps until the next job is due to be run.
Upon waking:
- it runs each entry that is active on that second
- it calculates the next run times for the jobs that were run
- it re-sorts the array of entries by next activation time.
- it goes to sleep until the soonest job.
*/
package cron

View File

@ -1,377 +0,0 @@
package cron
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
// Configuration options for creating a parser. Most options specify which
// fields should be included, while others enable features. If a field is not
// included the parser will assume a default value. These options do not change
// the order fields are parse in.
type ParseOption int
const (
Second ParseOption = 1 << iota // Seconds field, default 0
Minute // Minutes field, default 0
Hour // Hours field, default 0
Dom // Day of month field, default *
Month // Month field, default *
Dow // Day of week field, default *
DowOptional // Optional day of week field, default *
Descriptor // Allow descriptors such as @monthly, @weekly, etc.
)
var places = []ParseOption{
Second,
Minute,
Hour,
Dom,
Month,
Dow,
}
var defaults = []string{
"0",
"0",
"0",
"*",
"*",
"*",
}
// A custom Parser that can be configured.
type Parser struct {
options ParseOption
optionals int
}
// Creates a custom Parser with custom options.
//
// // Standard parser without descriptors
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
// sched, err := specParser.Parse("0 0 15 */3 *")
//
// // Same as above, just excludes time fields
// subsParser := NewParser(Dom | Month | Dow)
// sched, err := specParser.Parse("15 */3 *")
//
// // Same as above, just makes Dow optional
// subsParser := NewParser(Dom | Month | DowOptional)
// sched, err := specParser.Parse("15 */3")
//
func NewParser(options ParseOption) Parser {
optionals := 0
if options&DowOptional > 0 {
options |= Dow
optionals++
}
return Parser{options, optionals}
}
// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
// It accepts crontab specs and features configured by NewParser.
func (p Parser) Parse(spec string) (Schedule, error) {
if spec[0] == '@' && p.options&Descriptor > 0 {
return parseDescriptor(spec)
}
// Figure out how many fields we need
max := 0
for _, place := range places {
if p.options&place > 0 {
max++
}
}
min := max - p.optionals
// Split fields on whitespace
fields := strings.Fields(spec)
// Validate number of fields
if count := len(fields); count < min || count > max {
if min == max {
return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
}
return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
}
// Fill in missing fields
fields = expandFields(fields, p.options)
var err error
field := func(field string, r bounds) uint64 {
if err != nil {
return 0
}
var bits uint64
bits, err = getField(field, r)
return bits
}
var (
second = field(fields[0], seconds)
minute = field(fields[1], minutes)
hour = field(fields[2], hours)
dayofmonth = field(fields[3], dom)
month = field(fields[4], months)
dayofweek = field(fields[5], dow)
)
if err != nil {
return nil, err
}
return &SpecSchedule{
Second: second,
Minute: minute,
Hour: hour,
Dom: dayofmonth,
Month: month,
Dow: dayofweek,
}, nil
}
func expandFields(fields []string, options ParseOption) []string {
n := 0
count := len(fields)
expFields := make([]string, len(places))
copy(expFields, defaults)
for i, place := range places {
if options&place > 0 {
expFields[i] = fields[n]
n++
}
if n == count {
break
}
}
return expFields
}
var standardParser = NewParser(
Minute | Hour | Dom | Month | Dow | Descriptor,
)
// ParseStandard returns a new crontab schedule representing the given standardSpec
// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
// pass 5 entries representing: minute, hour, day of month, month and day of week,
// in that order. It returns a descriptive error if the spec is not valid.
//
// It accepts
// - Standard crontab specs, e.g. "* * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func ParseStandard(standardSpec string) (Schedule, error) {
return standardParser.Parse(standardSpec)
}
var defaultParser = NewParser(
Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,
)
// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
//
// It accepts
// - Full crontab specs, e.g. "* * * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func Parse(spec string) (Schedule, error) {
return defaultParser.Parse(spec)
}
// getField returns an Int with the bits set representing all of the times that
// the field represents or error parsing field value. A "field" is a comma-separated
// list of "ranges".
func getField(field string, r bounds) (uint64, error) {
var bits uint64
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bit, err := getRange(expr, r)
if err != nil {
return bits, err
}
bits |= bit
}
return bits, nil
}
// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
err error
)
var extra uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra = starBit
} else {
start, err = parseIntOrName(lowAndHigh[0], r.names)
if err != nil {
return 0, err
}
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end, err = parseIntOrName(lowAndHigh[1], r.names)
if err != nil {
return 0, err
}
default:
return 0, fmt.Errorf("Too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step, err = mustParseInt(rangeAndStep[1])
if err != nil {
return 0, err
}
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
return 0, fmt.Errorf("Too many slashes: %s", expr)
}
if start < r.min {
return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
if step == 0 {
return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
}
return getBits(start, end, step) | extra, nil
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt, nil
}
}
return mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or returns an error.
func mustParseInt(expr string) (uint, error) {
num, err := strconv.Atoi(expr)
if err != nil {
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
}
return uint(num), nil
}
// getBits sets all bits in the range [min, max], modulo the given step size.
func getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// all returns all bits within the given bounds. (plus the star bit)
func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
func parseDescriptor(descriptor string) (Schedule, error) {
switch descriptor {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: 1 << months.min,
Dow: all(dow),
}, nil
case "@monthly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: all(months),
Dow: all(dow),
}, nil
case "@weekly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: 1 << dow.min,
}, nil
case "@daily", "@midnight":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}, nil
case "@hourly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: all(hours),
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}, nil
}
const every = "@every "
if strings.HasPrefix(descriptor, every) {
duration, err := time.ParseDuration(descriptor[len(every):])
if err != nil {
return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
}
return Every(duration), nil
}
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
}

12
vendor/vendor.json vendored
View File

@ -102,18 +102,18 @@
"revision": "e8fbd41c16b9c0468dbae8db2fe0161a21265b8a", "revision": "e8fbd41c16b9c0468dbae8db2fe0161a21265b8a",
"revisionTime": "2017-02-21T11:08:50Z" "revisionTime": "2017-02-21T11:08:50Z"
}, },
{
"checksumSHA1": "j22mTM0X/UI4kbff6RaPeMNH4XY=",
"path": "github.com/jakecoffman/cron",
"revision": "57ac9950da80b6e2c12df9042429278cf8c729eb",
"revisionTime": "2016-09-12T16:42:50Z"
},
{ {
"checksumSHA1": "iKPMvbAueGfdyHcWCgzwKzm8WVo=", "checksumSHA1": "iKPMvbAueGfdyHcWCgzwKzm8WVo=",
"path": "github.com/klauspost/cpuid", "path": "github.com/klauspost/cpuid",
"revision": "09cded8978dc9e80714c4d85b0322337b0a1e5e0", "revision": "09cded8978dc9e80714c4d85b0322337b0a1e5e0",
"revisionTime": "2016-03-02T07:53:16Z" "revisionTime": "2016-03-02T07:53:16Z"
}, },
{
"checksumSHA1": "PphQA6j/DEDbg3WIsdBDYALOOYs=",
"path": "github.com/robfig/cron",
"revision": "9585fd555638e77bba25f25db5c44b41f264aeb7",
"revisionTime": "2016-09-27T16:42:31Z"
},
{ {
"checksumSHA1": "1keN4Q9F8uHk/Gt5YXG0oCEecKM=", "checksumSHA1": "1keN4Q9F8uHk/Gt5YXG0oCEecKM=",
"path": "github.com/urfave/cli", "path": "github.com/urfave/cli",