mirror of
https://github.com/ouqiang/gocron.git
synced 2024-04-21 12:31:58 +00:00
Compare commits
26
Commits
v0.0.1-beta
..
v0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93bb9f90b1 | ||
|
|
a7fd364b7d | ||
|
|
c9eb4e31af | ||
|
|
0148fd424b | ||
|
|
ce6ba171e7 | ||
|
|
6cd09d7f38 | ||
|
|
0f5c4c3e2a | ||
|
|
428275f813 | ||
|
|
0be9269b8c | ||
|
|
f699a72c28 | ||
|
|
70bccfaea0 | ||
|
|
0bc4070762 | ||
|
|
0dfa998d11 | ||
|
|
1b4a6e8c62 | ||
|
|
190cae9ae7 | ||
|
|
d1ecbd9043 | ||
|
|
55060aca67 | ||
|
|
2b2d4b237d | ||
|
|
5276fb9074 | ||
|
|
98bf5ee058 | ||
|
|
95ea60f23c | ||
|
|
6f76d3b17e | ||
|
|
85996077ec | ||
|
|
7c3f6a58d4 | ||
|
|
bf07c6e2e2 | ||
|
|
b2ad507010 |
+1
-2
@@ -1,5 +1,4 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- master
|
||||
script: go test `go list ./... | grep -v vendor`
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 qiang.ou
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
## 功能特性
|
||||
* 支持任务CURD
|
||||
* crontab时间表达式,可精确到每秒
|
||||
* crontab时间表达式,精确到秒
|
||||
* 任务执行失败重试设置
|
||||
* 任务超时设置
|
||||
* 延时任务
|
||||
* 任务执行方式
|
||||
* 调用本机系统命令
|
||||
* 通过SSH执行远程命令
|
||||
@@ -21,28 +22,31 @@
|
||||

|
||||
|
||||
### 支持平台
|
||||
> Windows、Linux、OSX
|
||||
> Windows、Linux、Mac OS
|
||||
|
||||
### 环境要求
|
||||
> MySQL
|
||||
|
||||
|
||||
## 下载
|
||||
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-linux-amd64.tar.gz)
|
||||
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-darwin-amd64.tar.gz)
|
||||
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-windows-amd64.zip)
|
||||
|
||||
## 安装
|
||||
|
||||
|
||||
### 二进制安装
|
||||
1. [下载](https://pan.baidu.com/s/1pLNRndx#list/path=%2Fgocron), 解压压缩包
|
||||
1. 解压压缩包
|
||||
2. `cd 解压目录`
|
||||
3. 启动
|
||||
* Windows: `gocron.exe web`
|
||||
* Linux、OSX: `./gocron web`
|
||||
* Linux、Mac OS: `./gocron web`
|
||||
4. 浏览器访问 http://localhost:5920
|
||||
### 源码安装
|
||||
1. `go`语言版本1.7+
|
||||
2. `go get -d https://github.com/ouqiang/gocron`
|
||||
2. `go get -d github.com/ouqiang/gocron`
|
||||
3. 编译 `go build`
|
||||
4. 启动、访问方式同上
|
||||
5. 生成压缩包(Windows: gocron.zip, 其他平台: gocron.tar.gz)
|
||||
> ./build.sh -p 平台 -a CPU架构 例 ./build.sh -p darwin -a amd64
|
||||
|
||||
### 启动可选参数
|
||||
|
||||
@@ -54,3 +58,13 @@
|
||||
* 使用`https`访问保证数据传输安全, 可在web服务器如nginx中配置https,通过反向代理,访问内部的gocron
|
||||
* 网站访问设置IP白名单
|
||||
* SSH登录设置IP白名单
|
||||
|
||||
## 程序使用的组件
|
||||
* web框架 [Macaron](http://go-macaron.com/)
|
||||
* 定时任务调度 [cron](https://github.com/robfig/cron)
|
||||
* ORM [Xorm](https://github.com/go-xorm/xorm)
|
||||
* UI框架 [Semantic UI](https://semantic-ui.com/)
|
||||
* 依赖管理(所有依赖包放入vendor目录) [govendor](https://github.com/kardianos/govendor)
|
||||
|
||||
## 贡献
|
||||
欢迎提交PR
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
|
||||
# ./build.sh -p windows -a amd64
|
||||
# 参数含义
|
||||
# -p 指定平台(widows|linux|darwin)
|
||||
# -p 指定平台(windows|linux|darwin)
|
||||
# -a 指定体系架构(amd64|386), 默认amd64
|
||||
|
||||
|
||||
|
||||
+34
-3
@@ -71,6 +71,27 @@ func initModule() {
|
||||
// 初始化定时任务
|
||||
serviceTask := new(service.Task)
|
||||
serviceTask.Initialize()
|
||||
|
||||
// 初始化延时任务
|
||||
delayTaskEnabled, err := config.Key("delay.task.enable").Bool()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !delayTaskEnabled {
|
||||
return
|
||||
}
|
||||
delayTaskSlots, err := config.Key("delay.task.slots").Int()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delayTaskTick := config.Key("delay.task.tick").String()
|
||||
tick, err := time.ParseDuration(delayTaskTick)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
serviceDelayTask := new(service.DelayTask)
|
||||
serviceDelayTask.Initialize(tick, delayTaskSlots)
|
||||
}
|
||||
|
||||
// 解析端口
|
||||
@@ -93,12 +114,12 @@ func setEnvironment(ctx *cli.Context) {
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "prod":
|
||||
macaron.Env = macaron.PROD
|
||||
case "test":
|
||||
macaron.Env = macaron.TEST
|
||||
case "dev":
|
||||
macaron.Env = macaron.DEV
|
||||
default:
|
||||
macaron.Env = macaron.PROD
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,12 +145,22 @@ func shutdown() {
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
logger.Info("应用准备退出\n停止任务调度")
|
||||
logger.Info("应用准备退出")
|
||||
serviceTask := new(service.Task)
|
||||
// 停止所有任务调度
|
||||
logger.Info("停止定时任务调度")
|
||||
serviceTask.StopAll()
|
||||
delayTaskEnable, _ := app.Setting.Key("delay.task.enable").Bool()
|
||||
if delayTaskEnable {
|
||||
logger.Info("停止延时任务调度")
|
||||
serviceDelayTask := new(service.DelayTask)
|
||||
serviceDelayTask.Stop()
|
||||
}
|
||||
taskNumInRunning := service.TaskNum.Num()
|
||||
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
|
||||
if taskNumInRunning > 0 {
|
||||
logger.Info("等待所有任务执行完成后退出")
|
||||
}
|
||||
for {
|
||||
if taskNumInRunning <= 0 {
|
||||
break
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/ouqiang/gocron/cmd"
|
||||
)
|
||||
|
||||
const AppVersion = "0.0.1"
|
||||
const AppVersion = "0.2"
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
|
||||
// 延迟任务
|
||||
type DelayTask struct {
|
||||
Id int64 `xorm:"bigint pk autoincr"`
|
||||
Url string `xorm:"varchar(128) not null"`
|
||||
Params string `xorm:"varchar(256) not null default '' "`
|
||||
Delay int `xorm:"mediumint notnull default 0"` // 延迟时间
|
||||
Status Status `xorm:"tinyint notnull index(u_status_created) default 5"` // 状态 0:执行失败 1:执行中 2:执行成功 5: 待执行
|
||||
Created time.Time `xorm:"datetime notnull created index(u_status_created)"`
|
||||
Updated time.Time `xorm:"datetime updated"`
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
func (task *DelayTask) Create() (insertId int64, err error) {
|
||||
_, err = Db.Insert(task)
|
||||
if err == nil {
|
||||
insertId = task.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有待执行任务
|
||||
func (task *DelayTask) ActiveList(endTime time.Time) ([]DelayTask, error) {
|
||||
list := make([]DelayTask, 0)
|
||||
fields := "id,url,params,delay,created"
|
||||
err := Db.Where("status = ? AND created <= ?", Waiting, endTime.Format(DefaultTimeFormat)).Cols(fields).Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 获取待执行任务数量
|
||||
func (task *DelayTask) ActiveNum(endTime time.Time) (int, error) {
|
||||
count ,err := Db.Where("status = ? AND created <= ?", Waiting, endTime.Format(DefaultTimeFormat)).Count(task)
|
||||
|
||||
return int(count), err
|
||||
}
|
||||
|
||||
func (task *DelayTask) List(params CommonMap) ([]DelayTask, error) {
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]DelayTask, 0)
|
||||
session := Db.Desc("id")
|
||||
task.parseWhere(session, params)
|
||||
err := session.Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
|
||||
// 更新任务状态
|
||||
func (task *DelayTask) UpdateStatus(id int64, status Status) (int64, error) {
|
||||
return Db.Table(task).Id(id).Update(CommonMap{
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (task *DelayTask) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func (task *DelayTask) Remove(id int) (int64, error) {
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("created <= ?", t.Format(DefaultTimeFormat)).Delete(task)
|
||||
}
|
||||
|
||||
func (task *DelayTask) Total(params CommonMap) (int64, error) {
|
||||
session := Db.NewSession()
|
||||
task.parseWhere(session, params)
|
||||
return session.Count(task)
|
||||
}
|
||||
+3
-1
@@ -13,8 +13,9 @@ func (migration *Migration) Exec(dbName string) error {
|
||||
return errors.New("数据库不存在")
|
||||
}
|
||||
setting := new(Setting)
|
||||
task := new(Task)
|
||||
tables := []interface{}{
|
||||
&User{}, &Task{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
|
||||
&User{}, task, &DelayTask{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
|
||||
}
|
||||
for _, table := range tables {
|
||||
exist, err:= Db.IsTableExist(table)
|
||||
@@ -30,6 +31,7 @@ func (migration *Migration) Exec(dbName string) error {
|
||||
}
|
||||
}
|
||||
setting.InitBasicField()
|
||||
task.CreateTestTask()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
Finish Status = 2 // 完成
|
||||
Cancel Status = 3 // 取消
|
||||
Background Status = 4 // 后台运行
|
||||
Waiting Status = 5 // 等待中
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+28
-2
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
"github.com/ouqiang/gocron/modules/ssh"
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
)
|
||||
|
||||
type TaskProtocol int8
|
||||
@@ -29,9 +30,9 @@ type Task struct {
|
||||
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
|
||||
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
||||
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
|
||||
Status Status `xorm:"tinyint notnull default 0"` // 状态 1:正常 0:停止
|
||||
Created time.Time `xorm:"datetime notnull created"` // 创建时间
|
||||
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
|
||||
Status Status `xorm:"tinyint notnull default 1"` // 状态 1:正常 0:停止
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
@@ -58,8 +59,33 @@ func (task *Task) Create() (insertId int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// 新增测试任务
|
||||
func (task *Task) CreateTestTask() {
|
||||
// HTTP任务
|
||||
task.Name = "测试HTTP任务"
|
||||
task.Protocol = TaskHTTP
|
||||
task.Spec = "*/30 * * * * *"
|
||||
// 查询IP地址区域信息
|
||||
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
|
||||
task.Status = Enabled
|
||||
task.Create()
|
||||
|
||||
// 系统命令
|
||||
task.Id = 0
|
||||
task.Name = "测试系统命令任务"
|
||||
task.Protocol = TaskLocalCommand
|
||||
task.Spec = "@every 1m"
|
||||
task.Status = Enabled
|
||||
if utils.IsWindows() {
|
||||
task.Command = "dir"
|
||||
} else {
|
||||
task.Command = "ls"
|
||||
}
|
||||
task.Create()
|
||||
}
|
||||
|
||||
func (task *Task) UpdateBean(id int) (int64, error) {
|
||||
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,status,notify_status,notify_type,notify_receiver_id").Update(task)
|
||||
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,notify_status,notify_type,notify_receiver_id").Update(task)
|
||||
}
|
||||
|
||||
// 更新
|
||||
|
||||
+19
-2
@@ -72,9 +72,26 @@ func (taskLog *TaskLog) List(params CommonMap) ([]TaskLog, error) {
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 根据通知ID获取任务ID
|
||||
func (taskLog *TaskLog) GetTaskIdByNotifyId(notifyId string) (taskId int, err error) {
|
||||
exist, err := Db.Where("notify_id = ?", notifyId).Get(taskLog)
|
||||
if !exist || err != nil {
|
||||
return
|
||||
}
|
||||
taskId = taskLog.TaskId
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 清空表
|
||||
func (TaskLog *TaskLog) Clear() (int64, error) {
|
||||
return Db.Where("1=1").Delete(TaskLog);
|
||||
func (taskLog *TaskLog) Clear() (int64, error) {
|
||||
return Db.Where("1=1").Delete(taskLog);
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func (taskLog *TaskLog) Remove(id int) (int64, error) {
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Total(params CommonMap) (int64, error) {
|
||||
|
||||
@@ -25,7 +25,18 @@ func Get(url string, timeout int) ResponseWrapper {
|
||||
return request(req, timeout)
|
||||
}
|
||||
|
||||
func PostBody(url string, body string, timeout int) ResponseWrapper {
|
||||
func PostParams(url string,params string, timeout int) ResponseWrapper {
|
||||
buf := bytes.NewBufferString(params)
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
|
||||
|
||||
return request(req, timeout)
|
||||
}
|
||||
|
||||
func PostJson(url string, body string, timeout int) ResponseWrapper {
|
||||
buf := bytes.NewBufferString(body)
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
if err != nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message)
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Error("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
|
||||
logger.Errorf("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ func (slack *Slack) send(msg Message, slackUrl string, channel string) {
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
resp := httpclient.PostBody(slackUrl, formatBody, timeout)
|
||||
resp := httpclient.PostJson(slackUrl, formatBody, timeout)
|
||||
if resp.StatusCode == 200 {
|
||||
break;
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Error("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
|
||||
logger.Errorf("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package delaytask
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/Unknwon/paginater"
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"html/template"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
)
|
||||
|
||||
func Index(ctx *macaron.Context) {
|
||||
delayTaskModel := new(models.DelayTask)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := delayTaskModel.Total(queryParams)
|
||||
tasks, err := delayTaskModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
PageParams := fmt.Sprintf("status=%d&page_size=%d",
|
||||
queryParams["Status"], queryParams["PageSize"]);
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "延时任务列表"
|
||||
ctx.Data["Tasks"] = tasks
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.HTML(200, "task/delay_task")
|
||||
}
|
||||
|
||||
func Create(ctx *macaron.Context) string {
|
||||
url := ctx.QueryTrim("url")
|
||||
params := ctx.QueryTrim("params")
|
||||
delay := ctx.QueryInt("delay")
|
||||
json := utils.JsonResponse{}
|
||||
delayTaskEnabled, _ := app.Setting.Key("delay.task.enable").Bool()
|
||||
if !delayTaskEnabled {
|
||||
return json.CommonFailure("系统未开启延时任务")
|
||||
}
|
||||
if url == "" {
|
||||
return json.CommonFailure("url地址不能为空")
|
||||
}
|
||||
lowerUrl := strings.ToLower(url)
|
||||
if !strings.HasPrefix(lowerUrl, "http") &&
|
||||
!strings.HasPrefix(lowerUrl, "https") {
|
||||
return json.CommonFailure("无效的url地址")
|
||||
}
|
||||
if len(url) > 128 {
|
||||
return json.CommonFailure("url长度不能超过128")
|
||||
}
|
||||
maxDelay := 1 << 31
|
||||
if delay <= 0 || delay > maxDelay {
|
||||
return json.CommonFailure("无效的delay, 取值范围1-(2^31-1)")
|
||||
}
|
||||
if len(params) > 256 {
|
||||
return json.CommonFailure("params长度不能超过256")
|
||||
}
|
||||
|
||||
delayTask := new(models.DelayTask)
|
||||
delayTask.Url = url
|
||||
delayTask.Params = params
|
||||
delayTask.Delay = delay
|
||||
delayTask.Status = models.Waiting
|
||||
_, err := delayTask.Create()
|
||||
|
||||
if err != nil {
|
||||
return json.CommonFailure("添加失败", err)
|
||||
}
|
||||
|
||||
logger.Infof("新增延时任务#id-%d#url-%s#params-%s#delay-%d",
|
||||
delayTask.Id, delayTask.Url, delayTask.Params, delayTask.Delay)
|
||||
delayTaskService := new(service.DelayTask)
|
||||
delayTaskService.Add(*delayTask)
|
||||
|
||||
return json.Success("添加成功", nil)
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
month := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
if month < 1 || month > 12 {
|
||||
return json.CommonFailure("参数取值范围1-12")
|
||||
}
|
||||
delayTaskModel := new(models.DelayTask)
|
||||
_, err := delayTaskModel.Remove(month)
|
||||
if err != nil {
|
||||
return json.CommonFailure("删除失败", err)
|
||||
}
|
||||
|
||||
return json.Success("删除成功", nil)
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
status := ctx.QueryInt("status")
|
||||
if status >=0 {
|
||||
status -= 1
|
||||
}
|
||||
params["Status"] = status
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return params
|
||||
}
|
||||
@@ -113,6 +113,9 @@ func writeConfig(form InstallForm) error {
|
||||
"db.charset": "utf8",
|
||||
"allow_ips" : "",
|
||||
"app.name": "定时任务管理系统", // 应用名称
|
||||
"delay.task.enable": "false", // 是否开启延时任务
|
||||
"delay.task.slots": "3600", // 时间轮槽数量
|
||||
"delay.task.tick": "1s", // 时间轮每次转动的时间
|
||||
}
|
||||
|
||||
return setting.Write(dbConfig, app.AppConfig)
|
||||
|
||||
@@ -105,6 +105,14 @@ func UpdateMailServer(ctx *macaron.Context, form MailServerForm) string {
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func ClearMailServer(ctx *macaron.Context) string {
|
||||
jsonByte, _ := json.Marshal(MailServerForm{})
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateMailServer(string(jsonByte))
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func CreateMailUser(ctx *macaron.Context) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
email := ctx.QueryTrim("email")
|
||||
|
||||
+18
-4
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/go-macaron/gzip"
|
||||
"github.com/ouqiang/gocron/routers/manage"
|
||||
"github.com/ouqiang/gocron/routers/loginlog"
|
||||
"github.com/ouqiang/gocron/routers/delaytask"
|
||||
)
|
||||
|
||||
// 静态文件目录
|
||||
@@ -43,7 +44,7 @@ func Register(m *macaron.Macaron) {
|
||||
m.Post("/editPassword", user.UpdatePassword)
|
||||
})
|
||||
|
||||
// 任务
|
||||
// 定时任务
|
||||
m.Group("/task", func() {
|
||||
m.Get("/create", task.Create)
|
||||
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
|
||||
@@ -57,6 +58,11 @@ func Register(m *macaron.Macaron) {
|
||||
m.Get("/run/:id", task.Run)
|
||||
})
|
||||
|
||||
// 延时任务
|
||||
m.Group("/delaytask", func() {
|
||||
m.Get("", delaytask.Index)
|
||||
})
|
||||
|
||||
// 主机
|
||||
m.Group("/host", func() {
|
||||
m.Get("/create", host.Create)
|
||||
@@ -80,6 +86,7 @@ func Register(m *macaron.Macaron) {
|
||||
m.Get("/", manage.Mail)
|
||||
m.Get("/edit", manage.EditMail)
|
||||
m.Post("/server", binding.Bind(manage.MailServerForm{}), manage.UpdateMailServer)
|
||||
m.Post("/server/clear", manage.ClearMailServer)
|
||||
m.Post("/user", manage.CreateMailUser)
|
||||
m.Post("/user/remove/:id", manage.RemoveMailUser)
|
||||
})
|
||||
@@ -88,7 +95,10 @@ func Register(m *macaron.Macaron) {
|
||||
|
||||
// API
|
||||
m.Group("/api/v1", func() {
|
||||
m.Get("/tasklog/update-status", tasklog.UpdateStatus)
|
||||
m.Route("/tasklog/update-status", "GET,POST", tasklog.UpdateStatus)
|
||||
m.Post("/tasklog/remove/:id", tasklog.Remove)
|
||||
m.Post("/delaytask/push", delaytask.Create)
|
||||
m.Post("/delaytask/log/remove/:id", delaytask.Remove)
|
||||
});
|
||||
|
||||
// 404错误
|
||||
@@ -149,7 +159,9 @@ func RegisterMiddleware(m *macaron.Macaron) {
|
||||
})
|
||||
}
|
||||
|
||||
// 系统未安装,重定向到安装页面
|
||||
// region 自定义中间件
|
||||
|
||||
/** 系统未安装,重定向到安装页面 **/
|
||||
func checkAppInstall(m *macaron.Macaron) {
|
||||
m.Use(func(ctx *macaron.Context) {
|
||||
installUrl := "/install"
|
||||
@@ -195,7 +207,9 @@ func userAuth(ctx *macaron.Context, sess session.Store) {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置共享数据
|
||||
// endregion
|
||||
|
||||
/** 设置共享数据 **/
|
||||
func setShareData(ctx *macaron.Context, sess session.Store) {
|
||||
ctx.Data["URI"] = ctx.Req.URL.Path
|
||||
urlPath := strings.TrimPrefix(ctx.Req.URL.Path, "/")
|
||||
|
||||
+21
-27
@@ -13,6 +13,7 @@ import (
|
||||
"html/template"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/go-macaron/binding"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TaskForm struct {
|
||||
@@ -26,7 +27,6 @@ type TaskForm struct {
|
||||
RetryTimes int8
|
||||
HostId int16
|
||||
Remark string
|
||||
Status models.Status `binding:"In(1,2)"`
|
||||
NotifyStatus int8 `binding:In(1,2,3)`
|
||||
NotifyType int8 `binding:In(1,2)`
|
||||
NotifyReceiverId string
|
||||
@@ -82,11 +82,6 @@ func Create(ctx *macaron.Context) {
|
||||
// 编辑页面
|
||||
func Edit(ctx *macaron.Context) {
|
||||
id := ctx.ParamsInt(":id")
|
||||
hostModel := new(models.Host)
|
||||
hosts, err := hostModel.List(models.CommonMap{})
|
||||
if err != nil || len(hosts) == 0 {
|
||||
logger.Error(err)
|
||||
}
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
if err != nil || task.Id != id {
|
||||
@@ -95,11 +90,7 @@ func Edit(ctx *macaron.Context) {
|
||||
}
|
||||
ctx.Data["Task"] = task
|
||||
ctx.Data["Title"] = "编辑"
|
||||
ctx.Data["Hosts"] = hosts
|
||||
if len(hosts) > 0 {
|
||||
ctx.Data["FirstHostName"] = hosts[0].Name
|
||||
ctx.Data["FirstHostId"] = hosts[0].Id
|
||||
}
|
||||
setHostsToTemplate(ctx)
|
||||
ctx.HTML(200, "task/task_form")
|
||||
}
|
||||
|
||||
@@ -134,12 +125,8 @@ func Store(ctx *macaron.Context, form TaskForm) string {
|
||||
taskModel.Command = form.Command
|
||||
taskModel.Timeout = form.Timeout
|
||||
taskModel.Remark = form.Remark
|
||||
taskModel.Status = form.Status
|
||||
taskModel.Multi = form.Multi
|
||||
taskModel.RetryTimes = form.RetryTimes
|
||||
if taskModel.Status != models.Enabled {
|
||||
taskModel.Status = models.Disabled
|
||||
}
|
||||
if taskModel.Multi != 1 {
|
||||
taskModel.Multi = 0
|
||||
}
|
||||
@@ -148,14 +135,30 @@ func Store(ctx *macaron.Context, form TaskForm) string {
|
||||
taskModel.NotifyReceiverId = form.NotifyReceiverId
|
||||
taskModel.Spec = form.Spec
|
||||
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
|
||||
return json.CommonFailure("请至少选择一个接收者", err)
|
||||
return json.CommonFailure("请至少选择一个接收者")
|
||||
}
|
||||
if taskModel.Protocol == models.TaskHTTP && taskModel.Timeout == -1 {
|
||||
return json.CommonFailure("HTTP任务不支持后台运行", err)
|
||||
if taskModel.Protocol == models.TaskHTTP {
|
||||
command := strings.ToLower(taskModel.Command)
|
||||
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
|
||||
return json.CommonFailure("请输入正确的URL地址")
|
||||
}
|
||||
if taskModel.Timeout == -1 {
|
||||
return json.CommonFailure("HTTP任务不支持后台运行")
|
||||
}
|
||||
if taskModel.Timeout > 300 {
|
||||
return json.CommonFailure("HTTP任务超时时间不能超过300秒")
|
||||
}
|
||||
}
|
||||
|
||||
if taskModel.RetryTimes > 10 || taskModel.RetryTimes < 0 {
|
||||
return json.CommonFailure("任务重试次数取值0-10")
|
||||
}
|
||||
|
||||
|
||||
if taskModel.Protocol != models.TaskSSH {
|
||||
taskModel.HostId = 0
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
id, err = taskModel.Create()
|
||||
} else {
|
||||
@@ -165,11 +168,6 @@ func Store(ctx *macaron.Context, form TaskForm) string {
|
||||
return json.CommonFailure("保存失败", err)
|
||||
}
|
||||
|
||||
// 任务处于激活状态,加入调度管理
|
||||
if (taskModel.Status == models.Enabled) {
|
||||
addTaskToTimer(id)
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
}
|
||||
|
||||
@@ -273,8 +271,4 @@ func setHostsToTemplate(ctx *macaron.Context) {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Hosts"] = hosts
|
||||
if len(hosts) > 0 {
|
||||
ctx.Data["FirstHostName"] = hosts[0].Name
|
||||
ctx.Data["FirstHostId"] = hosts[0].Id
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package tasklog
|
||||
|
||||
// 任务日志
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
@@ -9,11 +11,10 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// @author qiang.ou<qingqianludao@gmail.com>
|
||||
// @date 2017/4/7-21:18
|
||||
|
||||
func Index(ctx *macaron.Context) {
|
||||
logModel := new(models.TaskLog)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
@@ -49,12 +50,29 @@ func Clear(ctx *macaron.Context) string {
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
month := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
if month < 1 || month > 12 {
|
||||
return json.CommonFailure("参数取值范围1-12")
|
||||
}
|
||||
taskLogModel := new(models.TaskLog)
|
||||
_, err := taskLogModel.Remove(month)
|
||||
if err != nil {
|
||||
return json.CommonFailure("删除失败", err)
|
||||
}
|
||||
|
||||
return json.Success("删除成功", nil)
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
func UpdateStatus(ctx *macaron.Context) string {
|
||||
id := ctx.QueryTrim("id")
|
||||
status := ctx.QueryInt("status")
|
||||
result := ctx.QueryTrim("result")
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
if id == "" {
|
||||
return json.CommonFailure("任务ID不能为空")
|
||||
}
|
||||
@@ -70,6 +88,26 @@ func UpdateStatus(ctx *macaron.Context) string {
|
||||
return json.CommonFailure("更新任务状态失败")
|
||||
}
|
||||
|
||||
// 发送通知
|
||||
taskId, err := taskLogModel.GetTaskIdByNotifyId(id)
|
||||
if err != nil || taskId <= 0 {
|
||||
logger.Error("异步任务回调#根据notify-id获取taskId失败", err)
|
||||
return json.Success("success", nil)
|
||||
}
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(taskId)
|
||||
if err != nil || task.Id <= 0 {
|
||||
logger.Error("异步任务回调#根据获取任务详情失败", err)
|
||||
return json.Success("success", nil)
|
||||
}
|
||||
|
||||
taskResult := service.TaskResult{}
|
||||
taskResult.Result = result
|
||||
if status == 0 {
|
||||
taskResult.Err = errors.New("error")
|
||||
}
|
||||
service.SendNotification(task, taskResult)
|
||||
|
||||
return json.Success("success", nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"time"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"math"
|
||||
"github.com/ouqiang/gocron/modules/httpclient"
|
||||
"strings"
|
||||
"github.com/ouqiang/timewheel"
|
||||
)
|
||||
|
||||
var tw *timewheel.TimeWheel
|
||||
|
||||
type DelayTask struct {}
|
||||
|
||||
// 从数据库中取出所有延迟任务
|
||||
func (task *DelayTask) Initialize(tick time.Duration, slots int) {
|
||||
tw = timewheel.New(tick, slots)
|
||||
tw.Start()
|
||||
taskModel := new(models.DelayTask)
|
||||
currentTime := time.Now()
|
||||
taskNum, err := taskModel.ActiveNum(currentTime)
|
||||
if err != nil {
|
||||
logger.Error("延迟任务初始化#获取待执行的任务失败", err)
|
||||
return
|
||||
}
|
||||
if taskNum == 0 {
|
||||
logger.Debugf("延迟任务初始化#待执行的任务数量为0")
|
||||
return
|
||||
}
|
||||
pageSize := 100
|
||||
totalPage := int( math.Ceil(float64(taskNum) / float64(pageSize)) )
|
||||
logger.Infof("延迟任务初始化#待执行的任务数量-%d#共%d页#每页取%d条", taskNum, totalPage, pageSize)
|
||||
taskModel.PageSize = pageSize
|
||||
for page := 1; page <= totalPage; page++ {
|
||||
taskModel.Page = page
|
||||
logger.Debugf("延迟任务初始化#取出任务列表#第%d页", page)
|
||||
taskList, err := taskModel.ActiveList(currentTime)
|
||||
if err != nil {
|
||||
logger.Error("延迟任务初始化#获取任务列表失败", err)
|
||||
}
|
||||
task.BatchAdd(taskList)
|
||||
}
|
||||
logger.Info("延迟任务初始化完成")
|
||||
}
|
||||
|
||||
// 批量添加任务
|
||||
func (task *DelayTask) BatchAdd(taskList []models.DelayTask) {
|
||||
for _, item := range(taskList) {
|
||||
task.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
func (task *DelayTask) Add(taskModel models.DelayTask) {
|
||||
currentTimestamp := time.Now().Unix()
|
||||
execTimestamp := taskModel.Created.Unix() + int64(taskModel.Delay)
|
||||
// 时间过期, 立即执行任务
|
||||
if execTimestamp <= currentTimestamp {
|
||||
go task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
|
||||
return
|
||||
}
|
||||
delay := execTimestamp - currentTimestamp
|
||||
tw.Add(time.Duration(delay) * time.Second, func() {
|
||||
task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
|
||||
})
|
||||
}
|
||||
|
||||
// 运行任务
|
||||
func (task *DelayTask) Run(id int64, url, params string) {
|
||||
taskModel := new(models.DelayTask)
|
||||
_, err := taskModel.UpdateStatus(id, models.Running)
|
||||
if err != nil {
|
||||
logger.Error("延迟任务开始执行#更新任务状态失败", err)
|
||||
return
|
||||
}
|
||||
timeout := 300
|
||||
tryTimes := 3
|
||||
success := false
|
||||
logger.Infof("延迟任务开始执行#id-%d#url-%s#params-%s", id, url, params)
|
||||
for i := 0; i < tryTimes; {
|
||||
response := httpclient.PostParams(url, params, timeout)
|
||||
if response.StatusCode == 200 && strings.TrimSpace(response.Body) == "success"{
|
||||
success = true
|
||||
break;
|
||||
}
|
||||
i++
|
||||
if i < tryTimes {
|
||||
logger.Errorf("延迟任务执行失败#重试第%d次#任务Id-%d#HTTP状态码-%d#HTTP-BODY-%s",
|
||||
i,id,response.StatusCode,response.Body)
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
}
|
||||
logger.Infof("延迟任务执行完成#id-%d", id)
|
||||
var status models.Status
|
||||
if success {
|
||||
status = models.Finish
|
||||
} else {
|
||||
status = models.Failure
|
||||
}
|
||||
_ ,err = taskModel.UpdateStatus(id, status)
|
||||
if err != nil {
|
||||
logger.Error("延迟任务执行完成#更新任务状态失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (task *DelayTask) Stop() {
|
||||
tw.Stop()
|
||||
}
|
||||
+13
-2
@@ -173,7 +173,13 @@ func (h *LocalCommandHandler) runOnUnix(taskModel models.TaskHost) (string, erro
|
||||
// HTTP任务
|
||||
type HTTPHandler struct{}
|
||||
|
||||
// http任务执行时间不超过300秒
|
||||
const HttpExecTimeout = 300
|
||||
|
||||
func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error) {
|
||||
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
|
||||
taskModel.Timeout = HttpExecTimeout
|
||||
}
|
||||
resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
|
||||
// 返回状态码非200,均为失败
|
||||
if resp.StatusCode != 200 {
|
||||
@@ -274,7 +280,9 @@ func createJob(taskModel models.TaskHost) cron.FuncJob {
|
||||
if taskLogId <= 0 {
|
||||
return
|
||||
}
|
||||
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
|
||||
taskResult := execJob(handler, taskModel)
|
||||
logger.Infof("任务完成#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
|
||||
afterExecJob(taskModel, taskResult, taskLogId)
|
||||
}
|
||||
|
||||
@@ -338,11 +346,11 @@ func afterExecJob(taskModel models.TaskHost, taskResult TaskResult, taskLogId in
|
||||
return
|
||||
}
|
||||
|
||||
sendNotification(taskModel, taskResult)
|
||||
SendNotification(taskModel, taskResult)
|
||||
}
|
||||
|
||||
// 发送任务结果通知
|
||||
func sendNotification(taskModel models.TaskHost, taskResult TaskResult) {
|
||||
func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
|
||||
var statusName string
|
||||
// 未开启通知
|
||||
if taskModel.NotifyStatus == 0 {
|
||||
@@ -352,6 +360,9 @@ func sendNotification(taskModel models.TaskHost, taskResult TaskResult) {
|
||||
// 执行失败才发送通知
|
||||
return
|
||||
}
|
||||
if taskModel.NotifyReceiverId == "" {
|
||||
return
|
||||
}
|
||||
if taskResult.Err != nil {
|
||||
statusName = "失败"
|
||||
} else {
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
</div>
|
||||
|
||||
<div class="ui primary submit button">保存</div>
|
||||
<a class="ui button" onclick="location.href='/task';">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,9 +90,17 @@
|
||||
$('.ui.form').form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
swal({
|
||||
title: '',
|
||||
text: "系统安装中.......",
|
||||
type: 'info',
|
||||
showConfirmButton: false
|
||||
});
|
||||
util.post('/install/store', fields, function(code, message) {
|
||||
swal('安装成功');
|
||||
location.href = "/";
|
||||
setTimeout(function() {
|
||||
location.href = "/";
|
||||
}, 2000)
|
||||
});
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="four fields">
|
||||
<div class="field">
|
||||
<label>
|
||||
主机名
|
||||
SMTP服务器
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="host" value="{{{.Mail.Host}}}">
|
||||
@@ -48,6 +48,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">保存</button>
|
||||
{{{if .Mail.Host}}}
|
||||
<a class="ui green button" onclick="clearMailServer()">删除</a>
|
||||
{{{end}}}
|
||||
<br><br><br>
|
||||
<div>
|
||||
<div class="content">邮箱用户</div><p></p>
|
||||
@@ -98,7 +101,7 @@
|
||||
util.post('/manage/mail/server',
|
||||
fields,
|
||||
function(code, message) {
|
||||
util.alertSuccess();
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
return false;
|
||||
@@ -188,5 +191,11 @@
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function clearMailServer() {
|
||||
util.post('/manage/mail/server/clear', {}, function(code, message) {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
@@ -0,0 +1,84 @@
|
||||
{{{ 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" . }}}
|
||||
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<div class="content">
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<form class="ui form">
|
||||
<div class="fields search">
|
||||
<div class="field">
|
||||
<select name="status">
|
||||
<option value="0">任务状态</option>
|
||||
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >失败</option>
|
||||
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
|
||||
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
|
||||
<option value="6" {{{if eq .Params.Status 5}}}selected{{{end}}}>待执行</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui linkedin submit button">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="ui pink table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务ID</th>
|
||||
<th>URL</th>
|
||||
<th>参数</th>
|
||||
<th>延迟时间</th>
|
||||
<th>创建时间</th>
|
||||
<th>完成时间</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{range $i, $v := .Tasks}}}
|
||||
<tr>
|
||||
<td>{{{.Id}}}</td>
|
||||
<td>{{{.Url}}}</td>
|
||||
<td>{{{.Params}}}</td>
|
||||
<td>{{{.Delay}}}秒</td>
|
||||
<td>{{{.Created.Format "2006-01-02 15:04:05" }}}</td>
|
||||
<td>
|
||||
{{{if or (eq .Status 0) (eq .Status 2) }}}
|
||||
{{{.Updated.Format "2006-01-02 15:04:05" }}}
|
||||
{{{end}}}
|
||||
</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 if eq .Status 5}}}
|
||||
<span style="color:#43A102">待执行</span>
|
||||
{{{end}}}
|
||||
</td>
|
||||
</tr>
|
||||
{{{end}}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ template "common/pagination" .}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
@@ -62,7 +62,7 @@
|
||||
<p>cron表达式: {{{.Spec}}}</p>
|
||||
<p>执行方式: {{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else if eq .Protocol 3}}}本地命令{{{end}}}</p>
|
||||
<p class="sensorStatus">命令:{{{.Command}}}</p>
|
||||
<p class="sensorStatus">超时时间:{{{if eq .Timeout -1}}}后台运行{{{else if eq .Timeout 0}}}{{{.Timeout}}}秒{{{else}}}不限制{{{end}}}</p>
|
||||
<p class="sensorStatus">超时时间:{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}秒{{{else}}}不限制{{{end}}}</p>
|
||||
<p>重试次数: {{{.RetryTimes}}}</p>
|
||||
<p class="sensorStatus">是否允许多实例运行:{{{if gt .Multi 0}}}是{{{else}}}否{{{end}}}</p>
|
||||
{{{if eq .Protocol 2}}}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
|
||||
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
|
||||
<option value="4" {{{if eq .Params.Status 3}}}selected{{{end}}}>取消</option>
|
||||
<option value="5" {{{if eq .Params.Status 3}}}selected{{{end}}}>后台运行</option>
|
||||
<option value="5" {{{if eq .Params.Status 4}}}selected{{{end}}}>后台运行</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -72,7 +72,7 @@
|
||||
<td>{{{.Name}}}</td>
|
||||
<td>{{{.Spec}}}</td>
|
||||
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else}}} 系统命令 {{{end}}}</td>
|
||||
<td>{{{if eq .Timeout -1}}}后台运行{{{else if eq .Timeout 0}}}{{{.Timeout}}}秒{{{else}}}不限制{{{end}}}</td>
|
||||
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}秒{{{else}}}不限制{{{end}}}</td>
|
||||
<td>{{{.RetryTimes}}}</td>
|
||||
<td>{{{.Hostname}}}</td>
|
||||
<td>
|
||||
@@ -98,7 +98,7 @@
|
||||
{{{end}}}
|
||||
</td>
|
||||
<td>
|
||||
{{{if and (ne .Status 1) (ne .Status 3)}}}
|
||||
{{{if or (eq .Status 2) (eq .Status 0)}}}
|
||||
<button class="ui small primary button"
|
||||
onclick="showResult('{{{.Name}}}', '{{{.Command}}}', '{{{.Result}}}')"
|
||||
>查看结果
|
||||
|
||||
@@ -56,14 +56,14 @@
|
||||
{{{range $i, $v := .Hosts}}}
|
||||
<option value="{{{.Id}}}" {{{if $.Task}}}{{{if eq $.Task.HostId .Id }}} selected {{{end}}} {{{end}}}>{{{.Alias}}}-{{{.Name}}}</option>
|
||||
{{{end}}}
|
||||
</select> <a class="ui blue button" href="/host/create">添加主机</a>
|
||||
</select> <a class="ui blue button" href="/host/create" target="_blank">添加主机</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>命令</label>
|
||||
<textarea rows="5" name="command">{{{.Task.Command}}}</textarea>
|
||||
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{{{.Task.Command}}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="six fields">
|
||||
@@ -81,27 +81,12 @@
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>允许多实例同时运行</label>
|
||||
<div class="ui blue message">
|
||||
前次任务未执行完成,本次任务是否执行
|
||||
</div>
|
||||
<select name="multi">
|
||||
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}>是</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.Multi 0}}}selected{{{end}}} {{{end}}}>否</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>任务状态</label>
|
||||
<div class="ui blue message">
|
||||
任务添加成功后,是否立即调度
|
||||
</div>
|
||||
<select name="status">
|
||||
<option value="2"{{{if .Task}}} {{{if eq .Task.Status 0}}}selected{{{end}}} {{{end}}}>停止</option>
|
||||
<option value="1" {{{if .Task}}} {{{if eq .Task.Status 1}}}selected{{{end}}} {{{end}}}>激活</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>任务通知</label>
|
||||
@@ -129,7 +114,7 @@
|
||||
<textarea rows="5" name="remark">{{{.Task.Remark}}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui primary submit button">保存</div>
|
||||
<div class="ui primary submit button">保存</div> <a class="ui button" onclick="location.href='/task';">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,14 +147,18 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
changeCommandPlaceholder();
|
||||
changeProtocol();
|
||||
showNotify();
|
||||
});
|
||||
|
||||
$('#protocol').change(function() {
|
||||
changeProtocol();
|
||||
changeCommandPlaceholder();
|
||||
changeProtocol();
|
||||
});
|
||||
|
||||
|
||||
|
||||
$('#task-status').change(function() {
|
||||
var selected = $(this).val();
|
||||
if (selected == 1) {
|
||||
@@ -185,6 +174,21 @@
|
||||
changeNotify();
|
||||
});
|
||||
|
||||
function changeCommandPlaceholder() {
|
||||
var selectedId = $('#protocol').val();
|
||||
switch (selectedId) {
|
||||
case '1':
|
||||
$('#command').attr('placeholder', '请输入URL地址');
|
||||
break;
|
||||
case '2':
|
||||
$('#command').attr('placeholder', '请输入shell命令');
|
||||
break;
|
||||
case '3':
|
||||
$('#command').attr('placeholder', '请输入系统命令');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function showNotify() {
|
||||
var notifyStatus = {{{.Task.NotifyStatus}}};
|
||||
if (notifyStatus > 0) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# set -x -u
|
||||
# 上传二进制包到七牛
|
||||
|
||||
if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then
|
||||
echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 打包
|
||||
for i in linux darwin windows
|
||||
do
|
||||
./build.sh -p $i
|
||||
if [[ ! $? ]];then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 身份认证
|
||||
qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY
|
||||
|
||||
# 上传
|
||||
for i in `ls gocron*.gz gocron*.zip`
|
||||
do
|
||||
# 上传文件 qrsctl put bucket key srcFile
|
||||
KEY=gocron/$i
|
||||
qrsctl put github $KEY $i
|
||||
if [[ ! $? ]];then
|
||||
break
|
||||
fi
|
||||
echo "刷新七牛CDN-" $QINIU_URL/$KEY
|
||||
qrsctl cdn/refresh $QINIU_URL/$KEY
|
||||
rm $i
|
||||
done
|
||||
|
||||
echo '打包并上传成功'
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 qiang.ou
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# timewheel
|
||||
Golang实现的时间轮
|
||||
|
||||
|
||||

|
||||
|
||||
# 安装
|
||||
|
||||
```shell
|
||||
go get -u github.com/ouqiang/timewheel
|
||||
```
|
||||
|
||||
# 使用
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/timewheel"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// tick刻度为1秒, 3600个槽
|
||||
tw := timewheel.New(1 * time.Second, 3600)
|
||||
tw.Start()
|
||||
tw.Add(5 * time.Second, func() {
|
||||
// do something
|
||||
})
|
||||
tw.Add(10 * time.Minute, func() {
|
||||
// do something
|
||||
})
|
||||
tw.Add(35 * time.Hour, func() {
|
||||
// do something
|
||||
})
|
||||
// 停止
|
||||
tw.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package timewheel
|
||||
|
||||
import (
|
||||
"time"
|
||||
"container/list"
|
||||
)
|
||||
|
||||
// @author qiang.ou<qingqianludao@gmail.com>
|
||||
|
||||
type TimeWheel struct {
|
||||
interval time.Duration
|
||||
ticker *time.Ticker
|
||||
slots []*list.List
|
||||
currentPos int
|
||||
slotNum int
|
||||
taskChannel chan Task
|
||||
stopChannel chan bool
|
||||
}
|
||||
|
||||
|
||||
type Task struct {
|
||||
delay time.Duration
|
||||
circle int
|
||||
job Job
|
||||
}
|
||||
|
||||
type Job func()
|
||||
|
||||
func New(interval time.Duration, slotNum int) *TimeWheel {
|
||||
if interval <= 0 || slotNum <= 0 {
|
||||
return nil
|
||||
}
|
||||
tw := &TimeWheel{
|
||||
interval: interval,
|
||||
slots: make([]*list.List, slotNum),
|
||||
currentPos: 0,
|
||||
slotNum: slotNum,
|
||||
taskChannel: make(chan Task),
|
||||
stopChannel: make(chan bool),
|
||||
}
|
||||
|
||||
tw.initSlots()
|
||||
|
||||
return tw
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) initSlots() {
|
||||
for i := 0; i < tw.slotNum; i++ {
|
||||
tw.slots[i] = list.New()
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) Start() {
|
||||
tw.ticker = time.NewTicker(tw.interval)
|
||||
go tw.start()
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) Add(delay time.Duration, job Job) {
|
||||
if delay < 0 || job == nil {
|
||||
return
|
||||
}
|
||||
tw.taskChannel <- Task{delay:delay, job: job}
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) Stop() {
|
||||
tw.stopChannel <- true
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) start() {
|
||||
for {
|
||||
select {
|
||||
case <- tw.ticker.C:
|
||||
tw.tickHandler()
|
||||
case task := <- tw.taskChannel:
|
||||
tw.addTask(&task)
|
||||
case <- tw.stopChannel:
|
||||
tw.ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) tickHandler() {
|
||||
l := tw.slots[tw.currentPos]
|
||||
tw.scanAndRunTask(l)
|
||||
if tw.currentPos == tw.slotNum - 1 {
|
||||
tw.currentPos = 0
|
||||
} else {
|
||||
tw.currentPos++
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) scanAndRunTask(l *list.List) {
|
||||
for e := l.Front(); e != nil; {
|
||||
task := e.Value.(*Task)
|
||||
if task.circle > 0 {
|
||||
task.circle--
|
||||
e = e.Next()
|
||||
continue
|
||||
}
|
||||
|
||||
go task.job()
|
||||
next := e.Next()
|
||||
l.Remove(e)
|
||||
e = next
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) addTask(task *Task) {
|
||||
pos, circle := tw.getPositionAndCircle(task.delay)
|
||||
task.circle = circle
|
||||
|
||||
tw.slots[pos].PushBack(task)
|
||||
}
|
||||
|
||||
func (tw *TimeWheel) getPositionAndCircle(d time.Duration) (pos int, circle int) {
|
||||
delaySeconds := int(d.Seconds())
|
||||
intervalSeconds := int(tw.interval.Seconds())
|
||||
circle = int(delaySeconds / intervalSeconds / tw.slotNum)
|
||||
pos = int(tw.currentPos + delaySeconds / intervalSeconds) % tw.slotNum
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Vendored
+6
@@ -132,6 +132,12 @@
|
||||
"revision": "09cded8978dc9e80714c4d85b0322337b0a1e5e0",
|
||||
"revisionTime": "2016-03-02T07:53:16Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "kIFW+u9fHefC8sWE4W9pYIfJv5k=",
|
||||
"path": "github.com/ouqiang/timewheel",
|
||||
"revision": "c28ec761087c32fd75ad7514db2a4988d5c872d9",
|
||||
"revisionTime": "2017-05-14T12:16:09Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "1keN4Q9F8uHk/Gt5YXG0oCEecKM=",
|
||||
"path": "github.com/urfave/cli",
|
||||
|
||||
Reference in New Issue
Block a user