替换定时任务库

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
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/routers"
"github.com/urfave/cli"
@ -14,8 +11,6 @@ import (
"os/exec"
"syscall"
"github.com/ouqiang/gocron/modules/logger"
"github.com/go-macaron/toolbox"
"strings"
)
// 1号进程id
@ -24,9 +19,6 @@ const InitProcess = 1
// web服务器默认端口
const DefaultPort = 5920
// 静态文件目录
const StaticDir = "public"
var CmdWeb = cli.Command{
Name: "server",
Usage: "start scheduler web server",
@ -63,44 +55,11 @@ func run(ctx *cli.Context) {
// 注册路由
routers.Register(m)
// 注册中间件.
registerMiddleware(m)
routers.RegisterMiddleware(m)
port := parsePort(ctx)
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 {
var port int = DefaultPort

View File

@ -33,6 +33,12 @@ func (host *Host) Delete(id int) (int64, error) {
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) {
host.parsePageAndPageSize()
list := make([]Host, 0)
@ -49,8 +55,6 @@ func (host *Host) AllList() ([]Host, error) {
return list, err
}
func (host *Host) Total() (int64, error) {
return Db.Count(host)
}

View File

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

View File

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

View File

@ -6,24 +6,23 @@ import (
// 任务执行日志
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"` // 任务名称
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol Protocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command
Type TaskType `xorm:"tinyint notnull default 1"` // 任务类型 1: 定时任务 2: 延时任务
Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command
Command string `xorm:"varchar(512) notnull"` // URL地址或shell命令
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
Delay int `xorm:"int notnull default 0"` // 延时任务,延时时间(单位秒)
Hostname string `xorm:"varchar(512) notnull defalut '' "` // SSH主机名逗号分隔
StartTime time.Time `xorm:"datetime created"` // 开始执行时间
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 '' "` // 执行结果
Page int `xorm:"-"`
PageSize int `xorm:"-"`
}
func (taskLog *TaskLog) Create() (insertId int, err error) {
func (taskLog *TaskLog) Create() (insertId int64, err error) {
taskLog.Status = Running
_, 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)
}
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})
}
@ -51,19 +50,24 @@ func (taskLog *TaskLog) List() ([]TaskLog, error) {
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() {
if task.Page <= 0 {
task.Page = Page
func (taskLog *TaskLog) Total() (int64, error) {
return Db.Count(taskLog)
}
func (taskLog *TaskLog) parsePageAndPageSize() {
if taskLog.Page <= 0 {
taskLog.Page = Page
}
if task.PageSize >= 0 || task.PageSize > MaxPageSize {
task.PageSize = PageSize
if taskLog.PageSize >= 0 || taskLog.PageSize > MaxPageSize {
taskLog.PageSize = PageSize
}
}
func (task *Task) pageLimitOffset() int {
return (task.Page - 1) * task.PageSize
}
func (taskLog *TaskLog) pageLimitOffset() int {
return (taskLog.Page - 1) * taskLog.PageSize
}

View File

@ -4,7 +4,6 @@ import (
"os"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/crontask"
"github.com/ouqiang/gocron/service"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/logger"
@ -67,7 +66,6 @@ func CreateInstallLock() error {
// 初始化资源
func InitResource() {
// 初始化定时任务
crontask.DefaultCronTask = crontask.NewCronTask()
serviceTask := new(service.Task)
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"` // 数据
}
type Json struct{}
type JsonResponse struct{}
const ResponseSuccess = 0
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)
}
func (j *Json) Failure(code int, message string) string {
func (j *JsonResponse) Failure(code int, message string) string {
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{
Code: code,
Message: message,

View File

@ -6,22 +6,24 @@ function Util() {
var util = {};
util.post = function(url, params, callback) {
// 用户认证失败
var AUTH_ERROR = -1;
var FAILURE = 1;
var SUCCESS = 0;
var FAILURE = 1;
var NOT_FOUND = 2;
var AUTH_ERROR = 3;
var FAILURE_MESSAGE = '操作失败';
$.post(
url,
params,
function(response) {
if (!response) {
}
if (response.code === undefined) {
swal(FAILURE_MESSAGE, '服务端返回值无法解析', '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;
}
if (response.code == FAILURE) {
@ -33,6 +35,36 @@ function Util() {
'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;
}
}
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/modules/utils"
"github.com/ouqiang/gocron/modules/logger"
"strconv"
)
func Index(ctx *macaron.Context) {
@ -14,40 +15,68 @@ func Index(ctx *macaron.Context) {
logger.Error(err)
}
ctx.Data["Title"] = "主机列表"
ctx.Data["URI"] = "/host"
ctx.Data["Hosts"] = hosts
ctx.HTML(200, "host/index")
}
func Create(ctx *macaron.Context) {
ctx.Data["Title"] = "添加主机"
ctx.Data["URI"] = "/host/create"
ctx.HTML(200, "host/create")
}
type HostForm struct {
Name string `binding:"Required"`
Alias string `binding:"Required"`
Username string `binding:"Required"`
Password string
Name string `binding:"Required;MaxSize(100)"`
Alias string `binding:"Required;MaxSize(32)"`
Username string `binding:"Required;MaxSize(32)"`
Password string `binding:"Required;MaxSize(64)"`
Port int `binding:"Required;Range(1-65535)"`
Remark string `binding:"Required"`
Remark string
}
func Store(ctx *macaron.Context, form HostForm) string {
json := utils.Json{}
json := utils.JsonResponse{}
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.Alias = form.Alias
hostModel.Username = form.Username
hostModel.Password = form.Password
hostModel.Port = form.Port
hostModel.Remark = form.Remark
_, err := hostModel.Create()
_, err = hostModel.Create()
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "保存失败")
return json.CommonFailure("保存失败", err)
}
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 {
DbType string `binding:"IN(mysql)"`
DbHost string `binding:"Required"`
DbType string `binding:"In(mysql)"`
DbHost string `binding:"Required;MaxSize(50)"`
DbPort int `binding:"Required;Range(1,65535)"`
DbUsername string `binding:"Required"`
DbPassword string `binding:"Required"`
DbName string `binding:"Required"`
DbTablePrefix string
AdminUsername string `binding:"Required;MinSize(3)"`
AdminPassword string `binding:"Required;MinSize(6)"`
AdminEmail string `binding:"Email"`
DbUsername string `binding:"Required;MaxSize(50)"`
DbPassword string `binding:"Required;MaxSize(30)"`
DbName string `binding:"Required;MaxSize(50)"`
DbTablePrefix string `binding:"MinSize(20)"`
AdminUsername string `binding:"Required;MaxSize(3)"`
AdminPassword string `binding:"Required;MaxSize(6)"`
AdminEmail string `binding:"Required;Email;MaxSize(50)"`
}
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 {
json := utils.Json{}
json := utils.JsonResponse{}
if app.Installed {
logger.Warn("系统重复安装")
return json.Failure(utils.ResponseFailure, "系统已安装!")
return json.CommonFailure("系统已安装!")
}
err := testDbConnection(form)
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "数据库连接失败")
return json.CommonFailure("数据库连接失败", err)
}
// 写入数据库配置
err = writeConfig(form)
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "数据库配置写入文件失败")
return json.CommonFailure("数据库配置写入文件失败", err)
}
app.InitDb()
@ -63,22 +60,19 @@ func Store(ctx *macaron.Context, form InstallForm) string {
migration := new(models.Migration)
err = migration.Exec(form.DbName)
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "创建数据库表失败")
return json.CommonFailure("创建数据库表失败", err)
}
// 创建管理员账号
err = createAdminUser(form)
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "创建管理员账号失败")
return json.CommonFailure("创建管理员账号失败", err)
}
// 创建安装锁
err = app.CreateInstallLock()
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "创建文件安装锁失败")
return json.CommonFailure("创建文件安装锁失败", err)
}
app.Installed = true

View File

@ -7,26 +7,45 @@ import (
"github.com/ouqiang/gocron/routers/task"
"github.com/ouqiang/gocron/routers/host"
"github.com/ouqiang/gocron/routers/tasklog"
"runtime"
"strconv"
"github.com/ouqiang/gocron/modules/utils"
"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) {
// 所有GET方法自动注册HEAD方法
m.SetAutoHead(true)
// 404错误
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错误
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 {
return "go home"
})
m.Get("/", Home)
// 系统安装
m.Group("/install", func() {
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.Get("/create", task.Create)
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
m.Get("", task.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.Post("/store", binding.Bind(host.HostForm{}), host.Store)
m.Get("", host.Index)
m.Post("/remove/:id", host.Remove)
})
// API接口
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 false
}

View File

@ -5,7 +5,8 @@ import (
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"strings"
"github.com/ouqiang/gocron/service"
"strconv"
)
func Index(ctx *macaron.Context) {
@ -16,7 +17,6 @@ func Index(ctx *macaron.Context) {
}
ctx.Data["Title"] = "任务列表"
ctx.Data["Tasks"] = tasks
ctx.Data["URI"] = "/task"
ctx.HTML(200, "task/index")
}
@ -25,52 +25,123 @@ func Create(ctx *macaron.Context) {
hosts, err := hostModel.List()
if err != nil || len(hosts) == 0 {
logger.Error(err)
ctx.Redirect("/host/create")
}
logger.Debug(hosts)
ctx.Data["Title"] = "任务管理"
ctx.Data["Hosts"] = hosts
ctx.Data["URI"] = "/task/create"
ctx.Data["FirstHostName"] = hosts[0].Name
ctx.Data["FirstHostId"] = hosts[0].Id
ctx.HTML(200, "task/create")
}
type TaskForm struct {
Name string `binding:"Required"`
Spec string `binding:"Required"`
Protocol models.Protocol `binding:"Required"`
Type models.TaskType `binding:"Required"`
Command string `binding:"Required"`
Timeout int
Delay int
HostId int16 `binding:"Required;"`
Name string `binding:"Required;"`
Spec string `binding:"Required;MaxSize(64)"`
Protocol models.TaskProtocol `binding:"In(1,2)"`
Command string `binding:"Required;MaxSize(512)"`
Timeout int `binding:"Range(0,86400)"`
HostId int16
Remark string
Status models.Status `binding:"In(1,0)"`
}
// 保存任务
func Store(ctx *macaron.Context, form TaskForm) string {
json := utils.Json{}
json := utils.JsonResponse{}
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.Spec = strings.Replace(form.Spec, "\n", "|||", 100)
taskModel.Protocol = form.Protocol
taskModel.Type = form.Type
taskModel.Command = form.Command
taskModel.Timeout = form.Timeout
taskModel.HostId = form.HostId
taskModel.Delay = form.Delay
taskModel.Remark = form.Remark
_, err := taskModel.Create()
taskModel.Status = form.Status
taskModel.Spec = form.Spec
insertId, err := taskModel.Create()
if err != nil {
logger.Error(err)
return json.Failure(utils.ResponseFailure, "保存失败")
return json.CommonFailure("保存失败", err)
}
// 任务处于激活状态,加入调度管理
if (taskModel.Status == models.Enabled) {
addTaskToTimer(insertId)
}
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"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
)
// @author qiang.ou<qingqianludao@gmail.com>
@ -17,6 +18,17 @@ func Index(ctx *macaron.Context) {
}
ctx.Data["Title"] = "任务日志"
ctx.Data["Logs"] = logs
ctx.Data["URI"] = "/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 (
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/crontask"
"github.com/robfig/cron"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/jakecoffman/cron"
)
var Cron *cron.Cron
type Task struct{}
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
func (task *Task) Initialize() {
Cron = cron.New()
Cron.Start()
taskModel := new(models.Task)
taskList, err := taskModel.ActiveList()
if err != nil {
@ -38,17 +41,10 @@ func (task *Task) Add(taskModel models.TaskHost) {
logger.Error("添加任务#不存在的任务协议编号", taskModel.Protocol)
return
}
// 定时任务
if taskModel.Type == models.Timing {
err := crontask.DefaultCronTask.Update(strconv.Itoa(taskModel.Id), taskModel.Spec, taskFunc)
if err != nil {
logger.Error(err)
}
} else if taskModel.Type == models.Delay {
// 延时任务
delay := time.Duration(taskModel.Delay) * time.Second
time.AfterFunc(delay, taskFunc)
}
cronName := strconv.Itoa(taskModel.Id)
Cron.RemoveJob(cronName)
Cron.AddFunc(taskModel.Spec, taskFunc, cronName)
}
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.Name = taskModel.Task.Name
taskLogModel.Spec = taskModel.Spec
taskLogModel.Protocol = taskModel.Protocol
taskLogModel.Type = taskModel.Type
taskLogModel.Command = taskModel.Command
taskLogModel.Timeout = taskModel.Timeout
taskLogModel.Delay = taskModel.Delay
taskLogModel.Hostname = taskModel.Name
taskLogModel.StartTime = time.Now()
taskLogModel.Status = models.Running
@ -121,7 +115,7 @@ func createTaskLog(taskModel models.TaskHost) (int, error) {
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)
var status models.Status
if err != nil {
@ -140,9 +134,9 @@ func updateTaskLog(taskLogId int, result string, err error) (int64, error) {
func createHandlerJob(taskModel models.TaskHost) cron.FuncJob {
var handler Handler = nil
switch taskModel.Protocol {
case models.HTTP:
case models.TaskHTTP:
handler = new(HTTPHandler)
case models.SSHCommand:
case models.TaskSSH:
handler = new(SSHCommandHandler)
}
if handler == nil {
@ -162,10 +156,10 @@ func createHandlerJob(taskModel models.TaskHost) cron.FuncJob {
if err == nil {
break
} 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 {
logger.Error("更新任务日志失败-", err)
}

View File

@ -1,12 +1,10 @@
</div>
</div>
<footer>
<div id="copyrights">
<div class="inset">
<div class="bigcontainer">
<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">
GitHub
</a>

View File

@ -12,6 +12,7 @@
<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/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>
</head>
<body>
@ -47,11 +48,11 @@
<div class="ui teal inverted menu">
<div class="bigcontainer">
<div class="right menu">
<a class="item" href="/"><i class="home icon"></i>首页</a>
<a class="item" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item" 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 "/"}}}active{{{end}}}" href="/"><i class="home icon"></i>首页</a>
<a class="item {{{if eq .URI "/task"}}}active{{{end}}}" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item {{{if eq .URI "/host"}}}active{{{end}}}" href="/host"><i class="linux 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 class="container">
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
{{{ template "common/header" . }}}
<div class="ui grid">
<!--the vertical menu-->
{{{template "task/menu" .}}}
<div class="twelve wide column">
<div class="pageHeader">
@ -38,21 +36,8 @@
<div class="default text">SSH-Command (执行shell命令)</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item active" data-value="2">SSH-Command (执行shell命令)</div>
<div class="item" data-value="1">HTTP (执行http-post请求)</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 class="item active" data-value="2">SSH</div>
<div class="item" data-value="1">HTTP</div>
</div>
</div>
</div>
@ -71,29 +56,38 @@
</div>
</div>
<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">
</div>
<div class="three fields">
<div class="field">
<label>任务超时时间 (单位秒,0不限制,不能超过24小时, 默认0)</label>
<input type="text" name="timeout" placeholder="60">
</div>
<div class="field">
<label>延时时间 (单位秒)</label>
<input type="text" name="delay" placeholder="60">
<label>任务超时时间 (单位秒,0不限制,不能超过24小时)</label>
<input type="text" name="timeout" placeholder="0" value="180">
</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">
<label>备注</label>
<textarea rows="5" name="remark" placeholder="数据库备份, 每天凌晨执行一次"></textarea>
<textarea rows="5" name="remark" placeholder="任务备注"></textarea>
</div>
</div>
<div class="ui primary submit button">提交</div>
</form>
</div>
<!--the newDevice form-->
</div>
@ -104,7 +98,6 @@
$('.ui.form').form(
{
onSuccess: function(event, fields) {
var util = new Util();
util.post('/task/store', fields, function(code, message) {
location.href = "/task"
});

View File

@ -1,8 +1,6 @@
{{{ template "common/header" . }}}
<div class="ui grid">
<!--the vertical menu-->
{{{template "task/menu" .}}}
<div class="twelve wide column">
<div class="pageHeader">
@ -15,45 +13,65 @@
</h3>
</div>
</div>
<table class="ui single line table">
<table class="ui violet table">
<thead>
<tr>
<th>任务名称</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>
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
<tr>
<td>{{{.Name}}}</td>
<td>{{{.Task.Name}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{.Protocol}}}</td>
<td>{{{.Type}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else}}} SSH {{{end}}}</td>
<td>{{{.Command}}}</td>
<td>{{{.Timeout}}}</td>
<td>{{{.Delay}}}</td>
<td>{{{.Hostname}}}</td>
<td>{{{.Alias}}}</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>
{{{end}}}
</tbody>
</table>
</div>
<!--the newDevice form-->
</div>
<script type="text/javascript">
$('.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>
{{{ template "common/footer" . }}}

View File

@ -1,5 +1,13 @@
{{{ 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">
<!--the vertical menu-->
{{{ template "task/menu" . }}}
@ -8,28 +16,25 @@
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<i class="large add icon"></i>
<div class="content">
任务日志
<button class="ui small teal button" onclick="clearLog()">清空日志</button>
</div>
</h3>
</div>
</div>
<table class="ui single line table">
<table class="ui pink table">
<thead>
<tr>
<th width="8%">任务名称</th>
<th width="8%">cron表达式</th>
<th width="8%">协议</th>
<th width="8%">任务类型</th>
<th width="8%">命令</th>
<th width="8%">超时时间(秒)</th>
<th width="8%">延迟时间(秒)</th>
<th width="8%">主机</th>
<th width="8%">开始时间</th>
<th width="8%">结束时间</th>
<th width="8%">状态</th>
<th width="8%">执行结果</th>
<th>任务名称</th>
<th>cron表达式</th>
<th>协议</th>
<th>超时时间(秒)</th>
<th>主机</th>
<th>开始时间</th>
<th>结束时间</th>
<th>状态</th>
<th>执行结果</th>
</tr>
</thead>
<tbody>
@ -37,30 +42,89 @@
<tr>
<td>{{{.Name}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{.Protocol}}}</td>
<td>{{{.Type}}}</td>
<td>{{{.Command}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else}}} SSH {{{end}}}</td>
<td>{{{.Timeout}}}</td>
<td>{{{.Delay}}}</td>
<td>{{{.SshHosts}}}</td>
<td>{{{.StartTime}}}</td>
<td>{{{.EndTime}}}</td>
<td>{{{.Status}}}</td>
<td>{{{.Result}}}</td>
<td>{{{.Hostname}}}</td>
<td>
{{{.StartTime.Format "2006-01-02 15:03:04" }}}
</td>
<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>
{{{end}}}
</tbody>
</table>
</div>
<!--the newDevice form-->
</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">
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(
{
onSuccess: function(event, fields) {
var util = new Util();
util.post('/host/store', fields, function(code, message) {
location.reload();
});
@ -108,5 +172,4 @@
inline : true
});
</script>
{{{ 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.
// 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.
func Every(duration time.Duration) ConstantDelaySchedule {
if duration < time.Second {
duration = time.Second
panic("cron/constantdelay: delays of less than a second are not supported: " +
duration.String())
}
return ConstantDelaySchedule{
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,

View File

@ -3,23 +3,22 @@
package cron
import (
"log"
"runtime"
"sort"
"time"
)
type entries []*Entry
// 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
// be inspected while running.
type Cron struct {
entries []*Entry
entries entries
stop chan struct{}
add chan *Entry
snapshot chan []*Entry
remove chan string
snapshot chan entries
running bool
ErrorLog *log.Logger
location *time.Location
}
// Job is an interface for submitted cron jobs.
@ -49,6 +48,9 @@ type Entry struct {
// The Job to run.
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
@ -70,21 +72,15 @@ func (s byTime) Less(i, j int) bool {
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 {
return NewWithLocation(time.Now().Location())
}
// NewWithLocation returns a new Cron job runner.
func NewWithLocation(location *time.Location) *Cron {
return &Cron{
entries: nil,
add: make(chan *Entry),
remove: make(chan string),
stop: make(chan struct{}),
snapshot: make(chan []*Entry),
snapshot: make(chan entries),
running: false,
ErrorLog: nil,
location: location,
}
}
@ -94,27 +90,53 @@ type FuncJob func()
func (f FuncJob) Run() { f() }
// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error {
return c.AddJob(spec, FuncJob(cmd))
func (c *Cron) AddFunc(spec string, cmd func(), name string) {
c.AddJob(spec, FuncJob(cmd), name)
}
// AddJob adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error {
schedule, err := Parse(spec)
if err != nil {
return err
// AddFunc adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job, name string) {
c.Schedule(Parse(spec), cmd, name)
}
// 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.
func (c *Cron) Schedule(schedule Schedule, cmd Job) {
func (c *Cron) Schedule(schedule Schedule, cmd Job, name string) {
entry := &Entry{
Schedule: schedule,
Job: cmd,
Name: name,
}
if !c.running {
i := c.entries.pos(entry.Name)
if i != -1 {
return
}
c.entries = append(c.entries, entry)
return
}
@ -132,37 +154,17 @@ func (c *Cron) Entries() []*Entry {
return c.entrySnapshot()
}
// Location gets the time zone location
func (c *Cron) Location() *time.Location {
return c.location
}
// Start the cron scheduler in its own go-routine, or no-op if already started.
// Start the cron scheduler in its own go-routine.
func (c *Cron) Start() {
if c.running {
return
}
c.running = true
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
// access to the 'running' state variable.
func (c *Cron) run() {
// Figure out the next activation times for each entry.
now := time.Now().In(c.location)
now := time.Now().Local()
for _, entry := range c.entries {
entry.Next = entry.Schedule.Next(now)
}
@ -180,53 +182,50 @@ func (c *Cron) run() {
effective = c.entries[0].Next
}
timer := time.NewTimer(effective.Sub(now))
select {
case now = <-timer.C:
now = now.In(c.location)
case now = <-time.After(effective.Sub(now)):
// Run every entry whose next time was this effective time.
for _, e := range c.entries {
if e.Next != effective {
break
}
go c.runWithRecovery(e.Job)
go e.Job.Run()
e.Prev = e.Next
e.Next = e.Schedule.Next(now)
e.Next = e.Schedule.Next(effective)
}
continue
case newEntry := <-c.add:
i := c.entries.pos(newEntry.Name)
if i != -1 {
break
}
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:
c.snapshot <- c.entrySnapshot()
case <-c.stop:
timer.Stop()
return
}
// 'now' should be updated after newEntry and snapshot cases.
now = time.Now().In(c.location)
timer.Stop()
now = time.Now().Local()
}
}
// Logs an error to stderr or to the configured error log
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.
// Stop the cron scheduler.
func (c *Cron) Stop() {
if !c.running {
return
}
c.stop <- struct{}{}
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
import "time"
import (
"time"
)
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// 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 {
if !added {
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)
@ -132,7 +134,7 @@ WRAP:
for 1<<uint(t.Second())&s.Second == 0 {
if !added {
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)
@ -151,6 +153,7 @@ func dayMatches(s *SpecSchedule, t time.Time) bool {
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
)
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
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",
"revisionTime": "2017-02-21T11:08:50Z"
},
{
"checksumSHA1": "j22mTM0X/UI4kbff6RaPeMNH4XY=",
"path": "github.com/jakecoffman/cron",
"revision": "57ac9950da80b6e2c12df9042429278cf8c729eb",
"revisionTime": "2016-09-12T16:42:50Z"
},
{
"checksumSHA1": "iKPMvbAueGfdyHcWCgzwKzm8WVo=",
"path": "github.com/klauspost/cpuid",
"revision": "09cded8978dc9e80714c4d85b0322337b0a1e5e0",
"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=",
"path": "github.com/urfave/cli",