mirror of https://github.com/ouqiang/gocron
替换定时任务库
parent
dfcedb7069
commit
7aecf0c228
43
cmd/web.go
43
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,8 @@
|
|||
package delaytask
|
||||
|
||||
import "gopkg.in/macaron.v1"
|
||||
|
||||
// 创建延时任务
|
||||
func Create(ctx *macaron.Context) {
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package routers
|
||||
|
||||
import "gopkg.in/macaron.v1"
|
||||
|
||||
// 首页
|
||||
func Home(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "首页"
|
||||
ctx.HTML(200, "home/index")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<div id="copyrights">
|
||||
<div class="inset">
|
||||
<div class="bigcontainer">
|
||||
<div class="fl">
|
||||
<p>© 2017 cron-scheduler
|
||||
<p>© 2017 gocron
|
||||
<i class="Github Alternate icon"></i><a href="https://github.com/ouqiang/cron-scheduler" target="_blank">
|
||||
GitHub
|
||||
</a>
|
||||
|
|
|
@ -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>
|
|
@ -1,5 +1,12 @@
|
|||
{{{ template "common/header" . }}}
|
||||
|
||||
404错误
|
||||
|
||||
<script>
|
||||
swal({
|
||||
title: "404 - NOT FOUND",
|
||||
text: "您访问的页面不存在",
|
||||
type: "warning"
|
||||
},
|
||||
function(){
|
||||
location.href = "/"
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
|
@ -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" . }}}
|
|
@ -0,0 +1,3 @@
|
|||
{{{ template "common/header" . }}}
|
||||
|
||||
{{{ template "common/footer" . }}}
|
|
@ -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"
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = "/";
|
||||
|
|
|
@ -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"
|
||||
});
|
||||
|
|
|
@ -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" . }}}
|
|
@ -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" . }}}
|
0
vendor/github.com/robfig/cron/LICENSE → vendor/github.com/jakecoffman/cron/LICENSE
generated
vendored
0
vendor/github.com/robfig/cron/LICENSE → vendor/github.com/jakecoffman/cron/LICENSE
generated
vendored
|
@ -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.
|
|
@ -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,
|
137
vendor/github.com/robfig/cron/cron.go → vendor/github.com/jakecoffman/cron/cron.go
generated
vendored
137
vendor/github.com/robfig/cron/cron.go → vendor/github.com/jakecoffman/cron/cron.go
generated
vendored
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
9
vendor/github.com/robfig/cron/spec.go → vendor/github.com/jakecoffman/cron/spec.go
generated
vendored
9
vendor/github.com/robfig/cron/spec.go → vendor/github.com/jakecoffman/cron/spec.go
generated
vendored
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue