From 7aecf0c22890ed4a1fcd5935d90b3ca200b1fc34 Mon Sep 17 00:00:00 2001 From: ouqiang Date: Thu, 13 Apr 2017 17:35:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/web.go | 43 +- models/host.go | 8 +- models/model.go | 2 + models/task.go | 70 ++-- models/task_log.go | 40 +- modules/app/app.go | 2 - modules/crontask/cron_task.go | 94 ----- modules/utils/json.go | 21 +- public/resource/javascript/main.js | 46 ++- public/resource/javascript/vue.min.js | 8 + routers/api/v1/delaytask/delay_task.go | 8 + routers/home.go | 9 + routers/host/host.go | 51 ++- routers/install/install.go | 38 +- routers/routers.go | 86 +++- routers/task/task.go | 113 +++++- routers/tasklog/task_log.go | 14 +- service/task.go | 36 +- templates/common/footer.html | 4 +- templates/common/header.html | 13 +- templates/error/404.html | 13 +- templates/error/500.html | 12 +- templates/home/index.html | 3 + templates/host/create.html | 1 - templates/host/index.html | 2 + templates/install/create.html | 1 - templates/task/create.html | 49 +-- templates/task/index.html | 44 +- templates/task/log.html | 119 ++++-- .../{robfig => jakecoffman}/cron/LICENSE | 0 vendor/github.com/jakecoffman/cron/README.md | 141 +++++++ .../cron/constantdelay.go | 5 +- .../{robfig => jakecoffman}/cron/cron.go | 137 ++++--- vendor/github.com/jakecoffman/cron/parser.go | 223 +++++++++++ .../{robfig => jakecoffman}/cron/spec.go | 9 +- vendor/github.com/robfig/cron/README.md | 2 - vendor/github.com/robfig/cron/doc.go | 129 ------ vendor/github.com/robfig/cron/parser.go | 377 ------------------ vendor/vendor.json | 12 +- 39 files changed, 1018 insertions(+), 967 deletions(-) delete mode 100644 modules/crontask/cron_task.go create mode 100644 public/resource/javascript/vue.min.js create mode 100644 routers/api/v1/delaytask/delay_task.go create mode 100644 routers/home.go create mode 100644 templates/home/index.html rename vendor/github.com/{robfig => jakecoffman}/cron/LICENSE (100%) create mode 100644 vendor/github.com/jakecoffman/cron/README.md rename vendor/github.com/{robfig => jakecoffman}/cron/constantdelay.go (83%) rename vendor/github.com/{robfig => jakecoffman}/cron/cron.go (68%) create mode 100644 vendor/github.com/jakecoffman/cron/parser.go rename vendor/github.com/{robfig => jakecoffman}/cron/spec.go (94%) delete mode 100644 vendor/github.com/robfig/cron/README.md delete mode 100644 vendor/github.com/robfig/cron/doc.go delete mode 100644 vendor/github.com/robfig/cron/parser.go diff --git a/cmd/web.go b/cmd/web.go index 04091d4..f7d30c4 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -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 diff --git a/models/host.go b/models/host.go index fb5774c..1571221 100644 --- a/models/host.go +++ b/models/host.go @@ -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) } diff --git a/models/model.go b/models/model.go index c036b28..fff9f5a 100644 --- a/models/model.go +++ b/models/model.go @@ -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) diff --git a/models/task.go b/models/task.go index a62d18d..ea8c033 100644 --- a/models/task.go +++ b/models/task.go @@ -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 +} \ No newline at end of file diff --git a/models/task_log.go b/models/task_log.go index 052ef76..f57d861 100644 --- a/models/task_log.go +++ b/models/task_log.go @@ -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 +} \ No newline at end of file diff --git a/modules/app/app.go b/modules/app/app.go index 0e43e6f..301c53d 100644 --- a/modules/app/app.go +++ b/modules/app/app.go @@ -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() } diff --git a/modules/crontask/cron_task.go b/modules/crontask/cron_task.go deleted file mode 100644 index 973ceb7..0000000 --- a/modules/crontask/cron_task.go +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/modules/utils/json.go b/modules/utils/json.go index bb321b1..a806997 100644 --- a/modules/utils/json.go +++ b/modules/utils/json.go @@ -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, diff --git a/public/resource/javascript/main.js b/public/resource/javascript/main.js index 5735f6a..f2f47c5 100644 --- a/public/resource/javascript/main.js +++ b/public/resource/javascript/main.js @@ -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; -} \ No newline at end of file +} + +var util = new Util(); \ No newline at end of file diff --git a/public/resource/javascript/vue.min.js b/public/resource/javascript/vue.min.js new file mode 100644 index 0000000..d0cecce --- /dev/null +++ b/public/resource/javascript/vue.min.js @@ -0,0 +1,8 @@ +/*! + * Vue.js v2.2.0 + * (c) 2014-2017 Evan You + * Released under the MIT License. + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function t(e){var t=parseFloat(e);return isNaN(t)?e:t}function n(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}function i(e,t){return Ei.call(e,t)}function o(e){return"string"==typeof e||"number"==typeof e}function a(e){var t=Object.create(null);return function(n){var r=t[n];return r||(t[n]=e(n))}}function s(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function c(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function u(e,t){for(var n in t)e[n]=t[n];return e}function l(e){return null!==e&&"object"==typeof e}function f(e){return Mi.call(e)===Pi}function p(e){for(var t={},n=0;n1?c(n):n;for(var r=c(arguments,1),i=0,o=n.length;i=0&&bo[n].id>e.id;)n--;bo.splice(Math.max(n,xo)+1,0,e)}else bo.push(e);wo||(wo=!0,Xi(he))}}function ge(e){Oo.clear(),ye(e,Oo)}function ye(e,t){var n,r,i=Array.isArray(e);if((i||l(e))&&Object.isExtensible(e)){if(e.__ob__){var o=e.__ob__.dep.id;if(t.has(o))return;t.add(o)}if(i)for(n=e.length;n--;)ye(e[n],t);else for(r=Object.keys(e),n=r.length;n--;)ye(e[r[n]],t)}}function _e(e,t,n){So.get=function(){return this[t][n]},So.set=function(e){this[t][n]=e},Object.defineProperty(e,n,So)}function be(e){e._watchers=[];var t=e.$options;t.props&&$e(e,t.props),t.methods&&Ae(e,t.methods),t.data?we(e):A(e._data={},!0),t.computed&&Ce(e,t.computed),t.watch&&Oe(e,t.watch)}function $e(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[],o=!e.$parent;uo.shouldConvert=o;var a=function(o){i.push(o);var a=R(o,t,n,e);O(r,o,a),o in e||_e(e,"_props",o)};for(var s in t)a(s);uo.shouldConvert=!0}function we(e){var t=e.$options.data;t=e._data="function"==typeof t?t.call(e):t||{},f(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,o=n.length;o--;)r&&i(r,n[o])||_(n[o])||_e(e,"_data",n[o]);A(t,!0)}function Ce(e,t){var n=e._computedWatchers=Object.create(null);for(var r in t){var i=t[r],o="function"==typeof i?i:i.get;n[r]=new Ao(e,o,d,To),r in e||xe(e,r,i)}}function xe(e,t,n){"function"==typeof n?(So.get=ke(t),So.set=d):(So.get=n.get?n.cache!==!1?ke(t):n.get:d,So.set=n.set?n.set:d),Object.defineProperty(e,t,So)}function ke(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),io.target&&t.depend(),t.value}}function Ae(e,t){e.$options.props;for(var n in t)e[n]=null==t[n]?d:s(t[n],e)}function Oe(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i-1:e instanceof RegExp&&e.test(t)}function yt(e,t){for(var n in e){var r=e[n];if(r){var i=mt(r.componentOptions);i&&!t(i)&&(_t(r),e[n]=null)}}}function _t(e){e&&(e.componentInstance._inactive||de(e.componentInstance,"deactivated"),e.componentInstance.$destroy())}function bt(e){var t={};t.get=function(){return Hi},Object.defineProperty(e,"config",t),e.util={warn:no,extend:u,mergeOptions:M,defineReactive:O},e.set=S,e.delete=T,e.nextTick=Xi,e.options=Object.create(null),Hi._assetTypes.forEach(function(t){e.options[t+"s"]=Object.create(null)}),e.options._base=e,u(e.options.components,Po),lt(e),ft(e),pt(e),ht(e)}function $t(e){for(var t=e.data,n=e,r=e;r.componentInstance;)r=r.componentInstance._vnode,r.data&&(t=wt(r.data,t));for(;n=n.parent;)n.data&&(t=wt(t,n.data));return Ct(t)}function wt(e,t){return{staticClass:xt(e.staticClass,t.staticClass),class:e.class?[e.class,t.class]:t.class}}function Ct(e){var t=e.class,n=e.staticClass;return n||t?xt(n,kt(t)):""}function xt(e,t){return e?t?e+" "+t:e:t||""}function kt(e){var t="";if(!e)return t;if("string"==typeof e)return e;if(Array.isArray(e)){for(var n,r=0,i=e.length;r-1?aa[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:aa[e]=/HTMLUnknownElement/.test(t.toString())}function St(e){if("string"==typeof e){var t=document.querySelector(e);return t?t:document.createElement("div")}return e}function Tt(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function Et(e,t){return document.createElementNS(ta[e],t)}function jt(e){return document.createTextNode(e)}function Nt(e){return document.createComment(e)}function It(e,t,n){e.insertBefore(t,n)}function Lt(e,t){e.removeChild(t)}function Dt(e,t){e.appendChild(t)}function Mt(e){return e.parentNode}function Pt(e){return e.nextSibling}function Rt(e){return e.tagName}function Ft(e,t){e.textContent=t}function Ht(e,t,n){e.setAttribute(t,n)}function Ut(e,t){var n=e.data.ref;if(n){var i=e.context,o=e.componentInstance||e.elm,a=i.$refs;t?Array.isArray(a[n])?r(a[n],o):a[n]===o&&(a[n]=void 0):e.data.refInFor?Array.isArray(a[n])&&a[n].indexOf(o)<0?a[n].push(o):a[n]=[o]:a[n]=o}}function Bt(e){return null==e}function Vt(e){return null!=e}function zt(e,t){return e.key===t.key&&e.tag===t.tag&&e.isComment===t.isComment&&!e.data==!t.data}function Jt(e,t,n){var r,i,o={};for(r=t;r<=n;++r)i=e[r].key,Vt(i)&&(o[i]=r);return o}function Kt(e){function t(e){return new vo(O.tagName(e).toLowerCase(),{},[],void 0,e)}function r(e,t){function n(){0===--n.listeners&&i(e)}return n.listeners=t,n}function i(e){var t=O.parentNode(e);t&&O.removeChild(t,e)}function a(e,t,n,r,i){if(e.isRootInsert=!i,!s(e,t,n,r)){var o=e.data,a=e.children,c=e.tag;Vt(c)?(e.elm=e.ns?O.createElementNS(e.ns,c):O.createElement(c,e),v(e),f(e,a,t),Vt(o)&&d(e,t),l(n,e.elm,r)):e.isComment?(e.elm=O.createComment(e.text),l(n,e.elm,r)):(e.elm=O.createTextNode(e.text),l(n,e.elm,r))}}function s(e,t,n,r){var i=e.data;if(Vt(i)){var o=Vt(e.componentInstance)&&i.keepAlive;if(Vt(i=i.hook)&&Vt(i=i.init)&&i(e,!1,n,r),Vt(e.componentInstance))return c(e,t),o&&u(e,t,n,r),!0}}function c(e,t){e.data.pendingInsert&&t.push.apply(t,e.data.pendingInsert),e.elm=e.componentInstance.$el,p(e)?(d(e,t),v(e)):(Ut(e),t.push(e))}function u(e,t,n,r){for(var i,o=e;o.componentInstance;)if(o=o.componentInstance._vnode,Vt(i=o.data)&&Vt(i=i.transition)){for(i=0;ip?(u=Bt(n[m+1])?null:n[m+1].elm,h(e,u,n,f,m,r)):f>m&&g(e,t,l,p)}function b(e,t,n,r){if(e!==t){if(t.isStatic&&e.isStatic&&t.key===e.key&&(t.isCloned||t.isOnce))return t.elm=e.elm,void(t.componentInstance=e.componentInstance);var i,o=t.data,a=Vt(o);a&&Vt(i=o.hook)&&Vt(i=i.prepatch)&&i(e,t);var s=t.elm=e.elm,c=e.children,u=t.children;if(a&&p(t)){for(i=0;i=0&&(m=e.charAt(h)," "===m);h--);m&&ma.test(m)||(l=!0)}}else void 0===o?(v=i+1,o=e.slice(0,i).trim()):t();if(void 0===o?o=e.slice(0,i).trim():0!==v&&t(),a)for(i=0;i=Ro}function gn(e){return 34===e||39===e}function yn(e){var t=1;for(Bo=Uo;!mn();)if(e=hn(),gn(e))_n(e);else if(91===e&&t++,93===e&&t--,0===t){Vo=Uo;break}}function _n(e){for(var t=e;!mn()&&(e=hn(),e!==t););}function bn(e,t,n){zo=n;var r=t.value,i=t.modifiers,o=e.tag,a=e.attrsMap.type;if("select"===o)Cn(e,r,i);else if("input"===o&&"checkbox"===a)$n(e,r,i);else if("input"===o&&"radio"===a)wn(e,r,i);else if("input"===o||"textarea"===o)xn(e,r,i);else if(!Hi.isReservedTag(o))return pn(e,r,i),!1;return!0}function $n(e,t,n){var r=n&&n.number,i=ln(e,"value")||"null",o=ln(e,"true-value")||"true",a=ln(e,"false-value")||"false";an(e,"checked","Array.isArray("+t+")?_i("+t+","+i+")>-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),un(e,ya,"var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+t+"=$$c}",null,!0)}function wn(e,t,n){var r=n&&n.number,i=ln(e,"value")||"null";i=r?"_n("+i+")":i,an(e,"checked","_q("+t+","+i+")"),un(e,ya,dn(t,i),null,!0)}function Cn(e,t,n){var r=n&&n.number,i='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",o="$event.target.multiple ? $$selectedVal : $$selectedVal[0]",a="var $$selectedVal = "+i+";";a=a+" "+dn(t,o),un(e,"change",a,null,!0)}function xn(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?ga:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=dn(t,l);c&&(f="if($event.target.composing)return;"+f),an(e,"value","("+t+")"),un(e,u,f,null,!0),(s||a||"number"===r)&&un(e,"blur","$forceUpdate()")}function kn(e){var t;e[ga]&&(t=zi?"change":"input",e[t]=[].concat(e[ga],e[t]||[]),delete e[ga]),e[ya]&&(t=Zi?"click":"change",e[t]=[].concat(e[ya],e[t]||[]),delete e[ya])}function An(e,t,n,r){if(n){var i=t,o=Jo;t=function(n){var a=1===arguments.length?i(n):i.apply(null,arguments);null!==a&&On(e,t,r,o)}}Jo.addEventListener(e,t,r)}function On(e,t,n,r){(r||Jo).removeEventListener(e,t,n)}function Sn(e,t){if(e.data.on||t.data.on){var n=t.data.on||{},r=e.data.on||{};Jo=t.elm,kn(n),q(n,r,An,On,t.context)}}function Tn(e,t){if(e.data.domProps||t.data.domProps){var n,r,i=t.elm,o=e.data.domProps||{},a=t.data.domProps||{};a.__ob__&&(a=t.data.domProps=u({},a));for(n in o)null==a[n]&&(i[n]="");for(n in a)if(r=a[n],"textContent"!==n&&"innerHTML"!==n||(t.children&&(t.children.length=0),r!==o[n]))if("value"===n){i._value=r;var s=null==r?"":String(r);En(i,t,s)&&(i.value=s)}else i[n]=r}}function En(e,t,n){return!e.composing&&("option"===t.tag||jn(e,n)||Nn(e,n))}function jn(e,t){return document.activeElement!==e&&e.value!==t}function Nn(e,n){var r=e.value,i=e._vModifiers;return i&&i.number||"number"===e.type?t(r)!==t(n):i&&i.trim?r.trim()!==n.trim():r!==n}function In(e){var t=Ln(e.style);return e.staticStyle?u(e.staticStyle,t):t}function Ln(e){return Array.isArray(e)?p(e):"string"==typeof e?$a(e):e}function Dn(e,t){var n,r={};if(t)for(var i=e;i.componentInstance;)i=i.componentInstance._vnode,i.data&&(n=In(i.data))&&u(r,n);(n=In(e.data))&&u(r,n);for(var o=e;o=o.parent;)o.data&&(n=In(o.data))&&u(r,n);return r}function Mn(e,t){var n=t.data,r=e.data;if(n.staticStyle||n.style||r.staticStyle||r.style){var i,o,a=t.elm,s=e.data.staticStyle,c=e.data.style||{},l=s||c,f=Ln(t.data.style)||{};t.data.style=f.__ob__?u({},f):f;var p=Dn(t,!0);for(o in l)null==p[o]&&xa(a,o,"");for(o in p)i=p[o],i!==l[o]&&xa(a,o,null==i?"":i)}}function Pn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Rn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function Fn(e){if(e){if("object"==typeof e){var t={};return e.css!==!1&&u(t,Sa(e.name||"v")),u(t,e),t}return"string"==typeof e?Sa(e):void 0}}function Hn(e){Ma(function(){Ma(e)})}function Un(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),Pn(e,t)}function Bn(e,t){e._transitionClasses&&r(e._transitionClasses,t),Rn(e,t)}function Vn(e,t,n){var r=zn(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===Ea?Ia:Da,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=Ea,l=a,f=o.length):t===ja?u>0&&(n=ja,l=u,f=c.length):(l=Math.max(a,u),n=l>0?a>u?Ea:ja:null,f=n?n===Ea?o.length:c.length:0);var p=n===Ea&&Pa.test(r[Na+"Property"]);return{type:n,timeout:l,propCount:f,hasTransform:p}}function Jn(e,t){for(;e.length1}function Yn(e,t){t.data.show||qn(t)}function Qn(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var o,a,s=0,c=e.options.length;s-1,a.selected!==o&&(a.selected=o);else if(h(er(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function Xn(e,t){for(var n=0,r=t.length;n',n.innerHTML.indexOf(t)>0}function vr(e){return Ga=Ga||document.createElement("div"),Ga.innerHTML=e,Ga.textContent}function hr(e,t){var n=t?Ms:Ds;return e.replace(n,function(e){return Ls[e]})}function mr(e,t){function n(t){f+=t,e=e.substring(t)}function r(){var t=e.match(ss);if(t){var r={tagName:t[1],attrs:[],start:f};n(t[0].length);for(var i,o;!(i=e.match(cs))&&(o=e.match(is));)n(o[0].length),r.attrs.push(o);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=f,r}}function i(e){var n=e.tagName,r=e.unarySlash;u&&("p"===s&&es(n)&&o(s),Xa(n)&&s===n&&o(n));for(var i=l(n)||"html"===n&&"head"===s||!!r,a=e.attrs.length,f=new Array(a),p=0;p=0&&c[i].lowerCasedTag!==o;i--);else i=0;if(i>=0){for(var a=c.length-1;a>=i;a--)t.end&&t.end(c[a].tag,n,r);c.length=i,s=i&&c[i-1].tag}else"br"===o?t.start&&t.start(e,[],!0,n,r):"p"===o&&(t.start&&t.start(e,[],!1,n,r),t.end&&t.end(e,n,r))}for(var a,s,c=[],u=t.expectHTML,l=t.isUnaryTag||Ri,f=0;e;){if(a=e,s&&Ns(s)){var p=s.toLowerCase(),d=Is[p]||(Is[p]=new RegExp("([\\s\\S]*?)(]*>)","i")),v=0,h=e.replace(d,function(e,n,r){return v=r.length,"script"!==p&&"style"!==p&&"noscript"!==p&&(n=n.replace(//g,"$1").replace(//g,"$1")),t.chars&&t.chars(n),""});f+=e.length-h.length,e=h,o(p,f-v,f)}else{var m=e.indexOf("<");if(0===m){if(fs.test(e)){var g=e.indexOf("-->");if(g>=0){n(g+3);continue}}if(ps.test(e)){var y=e.indexOf("]>");if(y>=0){n(y+2);continue}}var _=e.match(ls);if(_){n(_[0].length);continue}var b=e.match(us);if(b){var $=f;n(b[0].length),o(b[1],$,f);continue}var w=r();if(w){i(w);continue}}var C=void 0,x=void 0,k=void 0;if(m>=0){for(x=e.slice(m);!(us.test(x)||ss.test(x)||fs.test(x)||ps.test(x)||(k=x.indexOf("<",1),k<0));)m+=k,x=e.slice(m);C=e.substring(0,m),n(m)}m<0&&(C=e,e=""),t.chars&&C&&t.chars(C)}if(e===a){t.chars&&t.chars(e);break}}o()}function gr(e,t){var n=t?Fs(t):Ps;if(n.test(e)){for(var r,i,o=[],a=n.lastIndex=0;r=n.exec(e);){i=r.index,i>a&&o.push(JSON.stringify(e.slice(a,i)));var s=tn(r[1].trim());o.push("_s("+s+")"),a=i+r[0].length}return a0,Ki=Vi&&Vi.indexOf("edge/")>0,qi=Vi&&Vi.indexOf("android")>0,Wi=Vi&&/iphone|ipad|ipod|ios/.test(Vi),Zi=Vi&&/chrome\/\d+/.test(Vi)&&!Ki,Gi=function(){return void 0===Oi&&(Oi=!Bi&&"undefined"!=typeof global&&"server"===global.process.env.VUE_ENV),Oi},Yi=Bi&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Qi="undefined"!=typeof Symbol&&y(Symbol)&&"undefined"!=typeof Reflect&&y(Reflect.ownKeys),Xi=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t1&&(t[n[0].trim()]=n[1].trim())}}),t}),wa=/^--/,Ca=/\s*!important$/,xa=function(e,t,n){wa.test(t)?e.style.setProperty(t,n):Ca.test(n)?e.style.setProperty(t,n.replace(Ca,""),"important"):e.style[Aa(t)]=n},ka=["Webkit","Moz","ms"],Aa=a(function(e){if(Ko=Ko||document.createElement("div"),e=Ni(e),"filter"!==e&&e in Ko.style)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=0;n\/=]+)/,ns=/(?:=)/,rs=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],is=new RegExp("^\\s*"+ts.source+"(?:\\s*("+ns.source+")\\s*(?:"+rs.join("|")+"))?"),os="[a-zA-Z_][\\w\\-\\.]*",as="((?:"+os+"\\:)?"+os+")",ss=new RegExp("^<"+as),cs=/^\s*(\/?)>/,us=new RegExp("^<\\/"+as+"[^>]*>"),ls=/^]+>/i,fs=/^ - {{{template "task/menu" .}}}
-
- -
- -
@@ -71,29 +56,38 @@
- +
- - -
-
- - + +
-
+
+
+ + +
+
+
- +
提交
- @@ -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" }); diff --git a/templates/task/index.html b/templates/task/index.html index 5831884..0489796 100644 --- a/templates/task/index.html +++ b/templates/task/index.html @@ -1,8 +1,6 @@ {{{ template "common/header" . }}}
- - {{{template "task/menu" .}}}
- +
- - + - + {{{range $i, $v := .Tasks}}} - + - - + - - + - + + {{{end}}}
任务名称 cron表达式 协议任务类型命令命令 超时时间(秒)延迟时间(秒) 主机 备注 状态操作
{{{.Name}}}{{{.Task.Name}}} {{{.Spec}}}{{{.Protocol}}}{{{.Type}}}{{{if eq .Protocol 1}}} HTTP {{{else}}} SSH {{{end}}} {{{.Command}}} {{{.Timeout}}}{{{.Delay}}}{{{.Hostname}}}{{{.Alias}}} {{{.Remark}}}{{{.Status}}}{{{if eq .Status 1}}} {{{else}}} {{{end}}} + {{{if eq .Status 1}}} + + {{{else}}} + + {{{end}}} + + +
- {{{ template "common/footer" . }}} \ No newline at end of file diff --git a/templates/task/log.html b/templates/task/log.html index bd6ae80..0707bfd 100644 --- a/templates/task/log.html +++ b/templates/task/log.html @@ -1,5 +1,13 @@ {{{ template "common/header" . }}} - +
{{{ template "task/menu" . }}} @@ -8,28 +16,25 @@ - +
- - - - - - - - - - - - + + + + + + + + + @@ -37,30 +42,89 @@ - - - + - - - - - - + + + + + {{{end}}}
任务名称cron表达式协议任务类型命令超时时间(秒)延迟时间(秒)主机开始时间结束时间状态执行结果任务名称cron表达式协议超时时间(秒)主机开始时间结束时间状态执行结果
{{{.Name}}} {{{.Spec}}}{{{.Protocol}}}{{{.Type}}}{{{.Command}}}{{{if eq .Protocol 1}}} HTTP {{{else}}} SSH {{{end}}} {{{.Timeout}}}{{{.Delay}}}{{{.SshHosts}}}{{{.StartTime}}}{{{.EndTime}}}{{{.Status}}}{{{.Result}}}{{{.Hostname}}} + {{{.StartTime.Format "2006-01-02 15:03:04" }}} + + {{{.EndTime.Format "2006-01-02 15:03:04" }}} + + {{{if eq .Status 2}}} + 成功 + {{{else if eq .Status 1}}} + 执行中 + {{{else if eq .Status 0}}} + 失败 + {{{else}}} + 待执行 + {{{end}}} + + +
- +
+ +
+ + - {{{ template "common/footer" . }}} \ No newline at end of file diff --git a/vendor/github.com/robfig/cron/LICENSE b/vendor/github.com/jakecoffman/cron/LICENSE similarity index 100% rename from vendor/github.com/robfig/cron/LICENSE rename to vendor/github.com/jakecoffman/cron/LICENSE diff --git a/vendor/github.com/jakecoffman/cron/README.md b/vendor/github.com/jakecoffman/cron/README.md new file mode 100644 index 0000000..d29a360 --- /dev/null +++ b/vendor/github.com/jakecoffman/cron/README.md @@ -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 | 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 + +where `` 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. diff --git a/vendor/github.com/robfig/cron/constantdelay.go b/vendor/github.com/jakecoffman/cron/constantdelay.go similarity index 83% rename from vendor/github.com/robfig/cron/constantdelay.go rename to vendor/github.com/jakecoffman/cron/constantdelay.go index cd6e7b1..7f28900 100644 --- a/vendor/github.com/robfig/cron/constantdelay.go +++ b/vendor/github.com/jakecoffman/cron/constantdelay.go @@ -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, diff --git a/vendor/github.com/robfig/cron/cron.go b/vendor/github.com/jakecoffman/cron/cron.go similarity index 68% rename from vendor/github.com/robfig/cron/cron.go rename to vendor/github.com/jakecoffman/cron/cron.go index f8d325d..9e3f378 100644 --- a/vendor/github.com/robfig/cron/cron.go +++ b/vendor/github.com/jakecoffman/cron/cron.go @@ -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 } diff --git a/vendor/github.com/jakecoffman/cron/parser.go b/vendor/github.com/jakecoffman/cron/parser.go new file mode 100644 index 0000000..7d0ee9d --- /dev/null +++ b/vendor/github.com/jakecoffman/cron/parser.go @@ -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 +} diff --git a/vendor/github.com/robfig/cron/spec.go b/vendor/github.com/jakecoffman/cron/spec.go similarity index 94% rename from vendor/github.com/robfig/cron/spec.go rename to vendor/github.com/jakecoffman/cron/spec.go index aac9a60..cb37433 100644 --- a/vendor/github.com/robfig/cron/spec.go +++ b/vendor/github.com/jakecoffman/cron/spec.go @@ -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< 0 dowMatch bool = 1< 0 ) + if s.Dom&starBit > 0 || s.Dow&starBit > 0 { return domMatch && dowMatch } diff --git a/vendor/github.com/robfig/cron/README.md b/vendor/github.com/robfig/cron/README.md deleted file mode 100644 index 157ed08..0000000 --- a/vendor/github.com/robfig/cron/README.md +++ /dev/null @@ -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) diff --git a/vendor/github.com/robfig/cron/doc.go b/vendor/github.com/robfig/cron/doc.go deleted file mode 100644 index dbdf501..0000000 --- a/vendor/github.com/robfig/cron/doc.go +++ /dev/null @@ -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 - -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 diff --git a/vendor/github.com/robfig/cron/parser.go b/vendor/github.com/robfig/cron/parser.go deleted file mode 100644 index b8a6d25..0000000 --- a/vendor/github.com/robfig/cron/parser.go +++ /dev/null @@ -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) -} diff --git a/vendor/vendor.json b/vendor/vendor.json index 8a62c3f..63601d2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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",