Compare commits

..
30 Commits
Author SHA1 Message Date
ouqiang 8ec80a02b6 feat($gocron-node): 增加token鉴权, #14 2017-09-06 22:46:56 +08:00
ouqiang 601f250882 docs(升级到v1.2): 2017-09-06 13:12:33 +08:00
ouqiang 019fee2cce feat($task): 调度器与任务节点支持HTTPS通信, #14 2017-09-06 11:27:54 +08:00
ouqiang d4e0898674 fix($task): 修复任务列表页总记录数显示错误 2017-09-06 09:43:13 +08:00
ouqiang d642c9641d feat($task): 任务批量开启、关闭、删除 2017-09-05 22:31:16 +08:00
ouqiang 2ba8cb67c8 feat($upgrade): 支持从v1.0版本升级到v1.1 2017-09-05 14:11:18 +08:00
ouqiang 350dc0881e feat($upgrade): 支持从旧版本升级, #13 2017-09-03 21:30:55 +08:00
qiang.ouandGitHub db1ef3b317 Update README.md 2017-09-03 11:25:25 +08:00
qiang.ouandGitHub 1c2696798c Update README.md 2017-09-03 11:20:45 +08:00
ouqiang 2b1c7f16cf feat($user): 用户登录增加图形验证码 2017-09-02 11:43:33 +08:00
ouqiang 337ee35357 feat($task): 支持任务同时在多个节点上运行
Closes #7
2017-08-06 22:49:24 +08:00
ouqiang b509fcec55 feat($gocron-node): *nix平台默认禁止以root用户运行任务节点 2017-08-06 01:00:41 +08:00
ouqiang 6e8622e4f3 feat($task): 替换子任务命令中的预定义占位符
子任务可根据主任务执行结果执行相应操作, 如主任务执行失败发送短信. 占位符{{.Code}}, {{.Message}}
2017-08-05 23:05:33 +08:00
ouqiang ff2228ed50 docs: 更新README 2017-07-22 09:24:50 +08:00
ouqiang 5d97ffad3b refactor: 删除守护进程模块, web访问日志输出到终端, Windows不再支持后台运行 2017-07-22 08:54:42 +08:00
qiang.ouandGitHub d13f1997dd Update README.md 2017-06-29 18:55:43 +08:00
qiang.ouandGitHub 5b8a8ae890 完善文档 2017-06-29 16:44:27 +08:00
ouqiang 3d61c7694b 修复map加锁无效 2017-06-26 18:07:56 +08:00
ouqiang 3e8c3a236e fix: 修复map加锁无效 2017-06-22 01:58:58 +08:00
ouqiang e6560e8483 style: 升级版本号到1.0 2017-06-21 23:49:12 +08:00
ouqiang 7e010d0aca feat: 增加定时任务开启、关闭API 2017-06-21 20:19:59 +08:00
ouqiang b8f13b4b0e refactor(延迟任务): 删除延迟任务模块
当任务较多时, 频繁读写数据库,数据库压力大, 计划拆分为独立项目, 用Redis实现持久化

BREAKING CHANGE: 不再支持延迟任务
2017-06-21 19:03:22 +08:00
qiang.ouandGitHub eb02804aec 更新文档 2017-06-14 17:55:34 +08:00
qiang.ouandGitHub eb7d8d7bf5 更新文档 2017-06-14 17:52:00 +08:00
qiang.ouandGitHub b5d476b339 更新文档 2017-06-14 17:48:37 +08:00
ouqiang a78d14693b Merge branch 'master' of github.com:ouqiang/gocron 2017-06-08 21:26:14 +08:00
ouqiang 157e899e06 增加主机连接测试 2017-06-08 21:25:42 +08:00
qiang.ouandGitHub 90a3136ce5 更新文档 2017-06-08 19:08:45 +08:00
ouqiang 22069aa156 增加任务依赖 2017-06-08 18:04:55 +08:00
ouqiang 3de70760a0 完善文档 2017-06-02 14:11:05 +08:00
58 changed files with 3254 additions and 1059 deletions
+5 -4
View File
@@ -26,12 +26,13 @@ _testmain.go
.idea
log/*
data/*
conf/install.lock
conf/app.ini
conf/ansible_hosts.ini
conf/*
profile/*
public/resource/javascript/vue.js
gocron
gocron.exe
gocron-node
gocron-node.exe
gocron-node.exe
node_modules
package.json
+5 -1
View File
@@ -1,4 +1,8 @@
language: go
go:
- 1.7.x
script: go test `go list ./... | grep -v vendor`
script: go test `go list ./... | grep -v vendor`
notifications:
on_success: never
on_failure: always
+63 -27
View File
@@ -2,23 +2,26 @@
# gocron - 定时任务管理系统
# 项目简介
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
原有的延时任务拆分为独立项目[延迟队列](https://github.com/ouqiang/delay-queue)
## 功能特性
* Web界面管理定时任务, 支持动态添加、删除、编辑任务
* Web界面管理定时任务, 支持动态添加、删除任务
* crontab时间表达式,精确到秒
* 任务执行失败重试设置
* 任务超时设置
* 延时任务
* 任务依赖配置
* 任务类型
* shell任务
> 远程服务器上执行shell命令, 调度器与任务执行器保持长连接
> 任务节点上执行shell命令, 支持任务同时在多个节点上运行
* HTTP任务
> 访问指定的URL地址
> 访问指定的URL地址, 由调度器直接执行, 不依赖任务节点
* 查看任务执行日志
* 任务执行结果通知, 支持邮件、Slack
### 截图
![流程图](https://raw.githubusercontent.com/ouqiang/gocron/master/scheduler.png)
![任务](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_task.png)
![Slack](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_slack.png)
@@ -30,50 +33,62 @@
## 下载
* 调度器(管理后台)
* [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)
* 任务执行器(安装在远程主机上, 执行shell命令需安装)
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-linux-amd64.tar.gz)
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-darwin-amd64.tar.gz)
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-windows-amd64.zip)
[v1.2](https://github.com/ouqiang/gocron/releases/tag/v1.2)
[版本升级](https://github.com/ouqiang/gocron/wiki/版本升级)
## 安装
### 二进制安装
1. 解压压缩包
1. 解压压缩包  
2. `cd 解压目录`
3. 启动
* 调度器启动
* Windows: `gocron.exe web`
* Linux、Mac OS: `./gocron web`
* 任务执行器启动
* Windows: `gocron-node.exe ip:port (默认0.0.0.0:5921)`
* Linux、Mac OS: `./gocron-node ip:port (默认0.0.0.0:5921)`
3. 启动
* 调度器启动
* Windows: `gocron.exe web`
* Linux、Mac OS: `./gocron web`
* 任务节点启动, 默认监听0.0.0.0:5921
* Windows: `gocron-node.exe`
* Linux、Mac OS: `./gocron-node`
4. 浏览器访问 http://localhost:5920
### 源码安装
1. `go`语言版本1.7+
2. `go get -d github.com/ouqiang/gocron`
3. 编译
* 调度器 `go build -tags gocron -o gocron`
* 任务执行器 `go build -tags node -o gocron-node`
* 任务节点 `go build -tags node -o gocron-node`
4. 启动、访问方式同上
### 命令
* gocron
* -v 查看版本
* gocron web
* --host 默认0.0.0.0
* -p 端口, 指定端口, 默认5920
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
* -d 后台运行
* -h 查看帮助
* gocron serv
* -s stop|status stop:停止gocron status:查看运行状态
* gocron-node ip:port, 默认0.0.0.0:5921
* gocron-node
* -allow-root *nix平台允许以root用户运行
* -s ip:port 监听地址
* -cert-file 证书文件
* -key-file 私钥文件
* -token
* -h 查看帮助
* -v 查看版本
## To Do List
- [x] 版本升级
- [x] 批量开启、关闭、删除任务
- [x] 调度器与任务节点通信支持https
- [ ] 任务分组
- [ ] 多用户
- [ ] 权限控制
- [ ] 新增任务API接口
## 程序使用的组件
* web框架 [Macaron](http://go-macaron.com/)
* 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/)
@@ -82,3 +97,24 @@
## 反馈
提交[issue](https://github.com/ouqiang/gocron/issues/new)
## ChangeLog
v1.2
--------
* 用户登录页增加图形验证码
* 支持从旧版本升级
* 任务批量开启、关闭、删除
* 调度器与任务节点支持HTTPS通信
* 修复任务列表页总记录数显示错误
v1.1
--------
* 任务可同时在多个节点上运行
* *nix平台默认禁止以root用户运行任务节点
* 子任务命令中增加预定义占位符, 子任务可根据主任务运行结果执行相应操作
* 删除守护进程模块
* Web访问日志输出到终端
+1 -1
View File
@@ -54,7 +54,7 @@ fi
echo '开始编译调度器'
if [[ $OS = 'windows' ]];then
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w -H windowsgui'
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
else
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
fi
+1 -1
View File
@@ -60,7 +60,7 @@ fi
echo '开始编译任务节点'
if [[ $OS = 'windows' ]];then
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w -H windowsgui' -o $EXEC_NAME
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w' -o $EXEC_NAME
else
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w' -o $EXEC_NAME
fi
-67
View File
@@ -1,67 +0,0 @@
package cmd
import (
"github.com/urfave/cli"
"fmt"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/app"
"os"
"syscall"
)
var CmdServ = cli.Command{
Name: "serv",
Usage: "manage gocron, ./gocron serv -s stop|status",
Action: runServ,
Flags: []cli.Flag{
cli.StringFlag{
Name: "s",
Value:"",
Usage: "stop|status",
},
},
}
func runServ(ctx *cli.Context) {
if utils.IsWindows() {
fmt.Println("not support on windows")
return
}
option := ctx.String("s")
if !utils.InStringSlice([]string{"stop", "status"}, option) {
fmt.Println("invalid option")
return
}
app.InitEnv()
pid := app.GetPid()
if pid <= 0 {
fmt.Println("not running")
return
}
process ,err := os.FindProcess(pid)
if err != nil {
fmt.Println("not running", err)
return
}
switch option {
case "stop":
stop(process)
case "status":
status(process)
}
}
func stop(process *os.Process) {
fmt.Println("stopping gocron......")
err := process.Signal(syscall.SIGTERM)
if err != nil {
fmt.Println("failed to kill process", err)
} else {
fmt.Println("stopped")
}
}
func status(process *os.Process) {
fmt.Printf("running, pid-[%d]\n", process.Pid)
}
+44 -89
View File
@@ -13,24 +13,23 @@ import (
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/setting"
"time"
"io"
"fmt"
"path/filepath"
"os/exec"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
)
// web服务器默认端口
const DefaultPort = 5920
const InitProcess = 1
var CmdWeb = cli.Command{
Name: "web",
Usage: "run web server",
Action: runWeb,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: "0.0.0.0",
Usage: "bind host",
},
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
@@ -41,62 +40,27 @@ var CmdWeb = cli.Command{
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
cli.BoolFlag{
Name: "d",
Usage: "-d=true, run as daemon process",
},
},
}
func runWeb(ctx *cli.Context) {
// 设置守护进程
becomeDaemon(ctx)
// 设置运行环境
setEnvironment(ctx)
// 初始化应用
app.InitEnv()
app.WritePid()
app.InitEnv(ctx.App.Version)
// 初始化模块 DB、定时任务等
initModule()
// 捕捉信号,配置热更新等
go catchSignal()
m := macaron.NewWithLogger(getWebLogWriter())
m := macaron.Classic()
// 注册路由
routers.Register(m)
// 注册中间件.
routers.RegisterMiddleware(m)
host := parseHost(ctx)
port := parsePort(ctx)
fmt.Println("server start")
m.Run(port)
}
func becomeDaemon(ctx *cli.Context) {
// 不支持windows
if utils.IsWindows() {
return
}
if !ctx.IsSet("d") {
return
}
if os.Getppid() == InitProcess {
// 子进程不再处理
return
}
filePath, _:= filepath.Abs(os.Args[0])
cmd := exec.Command(filePath, os.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
logger.Fatal("创建守护进程失败", err)
}
// 父进程退出, 子进程由init-1号进程收养
os.Exit(0)
m.Run(host, port)
}
func initModule() {
@@ -110,32 +74,15 @@ func initModule() {
}
app.Setting = config
// 初始化DB
models.Db = models.CreateDb()
// 版本升级
upgradeIfNeed()
// 初始化定时任务
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)
}
// 解析端口
@@ -151,6 +98,14 @@ func parsePort(ctx *cli.Context) int {
return port
}
func parseHost(ctx *cli.Context) string {
if ctx.IsSet("host") {
return ctx.String("host")
}
return "0.0.0.0"
}
func setEnvironment(ctx *cli.Context) {
var env string = "prod"
if ctx.IsSet("env") {
@@ -184,25 +139,10 @@ func catchSignal() {
}
}
func getWebLogWriter() io.Writer {
if macaron.Env == macaron.DEV {
return os.Stdout
}
logFile := app.LogDir + "/access.log"
var err error
w, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND ,0666)
if err != nil {
fmt.Printf("日志文件[%s]打开失败", logFile)
panic(err)
}
return w
}
// 应用退出
func shutdown() {
defer func() {
app.RemovePid()
logger.Info("已退出")
os.Exit(0)
}()
@@ -215,14 +155,6 @@ func shutdown() {
// 停止所有任务调度
logger.Info("停止定时任务调度")
serviceTask.StopAll()
delayTaskEnable, _ := app.Setting.Key("delay.task.enable").Bool()
if delayTaskEnable {
logger.Info("停止延时任务调度")
serviceDelayTask := new(service.DelayTask)
serviceDelayTask.Stop()
}
// 释放gRPC连接池
grpcpool.Pool.ReleaseAll()
taskNumInRunning := service.TaskNum.Num()
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
@@ -236,4 +168,27 @@ func shutdown() {
time.Sleep(3 * time.Second)
taskNumInRunning = service.TaskNum.Num()
}
// 释放gRPC连接池
grpcpool.Pool.ReleaseAll()
}
// 判断应用是否需要升级, 当存在版本号文件且版本小于app.VersionId时升级
func upgradeIfNeed() {
currentVersionId := app.GetCurrentVersionId()
// 没有版本号文件
if currentVersionId == 0 {
return;
}
if currentVersionId >= app.VersionId {
return
}
migration := new(models.Migration)
logger.Infof("版本升级开始, 当前版本号%d", currentVersionId)
migration.Upgrade(currentVersionId)
app.UpdateVersionFile()
logger.Infof("已升级到最新版本%d", app.VersionId)
}
View File
+46 -7
View File
@@ -5,15 +5,54 @@ package main
import (
"github.com/ouqiang/gocron/modules/rpc/server"
"os"
"flag"
"runtime"
"os"
"fmt"
"strings"
)
const AppVersion = "1.2"
func main() {
var addr string
if (len(os.Args) < 2) {
addr = "0.0.0.0:5921"
} else {
addr = os.Args[1]
var serverAddr string
var allowRoot bool
var version bool
var keyFile string
var certFile string
var token string
flag.BoolVar(&allowRoot, "allow-root", false, "./gocron-node -allow-root")
flag.StringVar(&serverAddr, "s", "0.0.0.0:5921", "./gocron-node -s ip:port")
flag.StringVar(&certFile, "cert-file", "", "./gocron-node -cert-file path")
flag.StringVar(&keyFile, "key-file", "", "./gocron-node -key-file path")
flag.StringVar(&token, "token", "", "./gocron-node -token")
flag.BoolVar(&version, "v", false, "./gocron-node -v")
flag.Parse()
if version {
fmt.Println(AppVersion)
os.Exit(0)
}
server.Start(addr)
certFile = strings.TrimSpace(certFile)
keyFile = strings.TrimSpace(keyFile)
if certFile != "" && keyFile == "" {
fmt.Println("missing argument key-file")
return
}
if keyFile != "" && certFile == "" {
fmt.Println("missing argument cert-file")
return
}
if runtime.GOOS != "windows" && os.Getuid() == 0 && !allowRoot {
fmt.Println("Do not run gocron-node as root user")
os.Exit(1)
}
server.Start(serverAddr, certFile, keyFile, token)
}
+1 -2
View File
@@ -10,7 +10,7 @@ import (
"github.com/ouqiang/gocron/cmd"
)
const AppVersion = "0.4"
const AppVersion = "1.2"
func main() {
app := cli.NewApp()
@@ -19,7 +19,6 @@ func main() {
app.Version = AppVersion
app.Commands = []cli.Command{
cmd.CmdWeb,
cmd.CmdServ,
}
app.Flags = append(app.Flags, []cli.Flag{}...)
app.Run(os.Args)
-85
View File
@@ -1,85 +0,0 @@
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 IN (?, ?) AND created <= ?", Waiting, Running, 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 IN (?, ?) AND created <= ?", Waiting, Running, 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)
}
+4 -1
View File
@@ -10,8 +10,11 @@ type Host struct {
Name string `xorm:"varchar(64) notnull"` // 主机名称
Alias string `xorm:"varchar(32) notnull default '' "` // 主机别名
Port int `xorm:"notnull default 22"` // 主机端口
CertFile string `xorm:"varchar(64) notnull default '' "`
Token string `xorm:"varchar(128) notnull default '' "`
Remark string `xorm:"varchar(100) notnull default '' "` // 备注
BaseModel `xorm:"-"`
Selected bool `xorm:"-"`
}
// 新增
@@ -25,7 +28,7 @@ func (host *Host) Create() (insertId int16, err error) {
}
func (host *Host) UpdateBean(id int16) (int64, error) {
return Db.ID(id).Cols("name,alias,port,remark").Update(host)
return Db.ID(id).Cols("name,alias,port,cert_file,token,remark").Update(host)
}
+113 -4
View File
@@ -2,20 +2,24 @@ package models
import (
"errors"
"fmt"
"github.com/ouqiang/gocron/modules/logger"
"github.com/go-xorm/xorm"
"strconv"
)
// 创建数据库表
type Migration struct{}
func (migration *Migration) Exec(dbName string) error {
// 首次安装, 创建数据库表
func (migration *Migration) Install(dbName string) error {
if !isDatabaseExist(dbName) {
return errors.New("数据库不存在")
}
setting := new(Setting)
task := new(Task)
tables := []interface{}{
&User{}, task, &DelayTask{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
&User{}, task, &TaskLog{}, &Host{}, setting,&LoginLog{},&TaskHost{},
}
for _, table := range tables {
exist, err:= Db.IsTableExist(table)
@@ -36,9 +40,114 @@ func (migration *Migration) Exec(dbName string) error {
return nil
}
// 创建数据库
// 判断数据库是否存在
func isDatabaseExist(name string) bool {
_, err := Db.Exec("use ?", name)
return err != nil
}
// 迭代升级数据库, 新建表、新增字段等
func (migration *Migration) Upgrade(oldVersionId int) {
versionIds := []int{110, 120}
upgradeFuncs := []func(*xorm.Session) error {
migration.upgradeFor110,
migration.upgradeFor120,
}
// 默认当前版本为v1.0
startIndex := 0
// 从当前版本的下一版本开始升级
for i, value := range versionIds {
if oldVersionId == value {
startIndex = i + 1
break;
}
}
length := len(versionIds)
if startIndex >= length {
return
}
session := Db.NewSession()
err := session.Begin()
if err != nil {
logger.Fatalf("开启事务失败-%s", err.Error())
}
for startIndex < length {
err = upgradeFuncs[startIndex](session)
if err == nil {
startIndex++
continue
}
dbErr := session.Rollback()
if dbErr != nil {
logger.Fatalf("事务回滚失败-%s",dbErr.Error())
}
logger.Fatal(err)
}
err = session.Commit()
if err != nil {
logger.Fatalf("提交事务失败-%s", err.Error())
}
}
// 升级到v1.1版本
func (migration *Migration) upgradeFor110(session *xorm.Session) error {
logger.Info("开始升级到v1.1")
// 创建表task_host
err := session.Sync2(new(TaskHost))
if err != nil {
return err
}
tableName := TablePrefix + "task"
// 把task对应的host_id写入task_host表
sql := fmt.Sprintf("SELECT id, host_id FROM %s WHERE host_id > 0", tableName)
results, err := session.Query(sql)
if err != nil {
return err
}
for _, value := range results {
taskHostModel := &TaskHost{}
taskId, err := strconv.Atoi(string(value["id"]))
if err != nil {
return err
}
hostId, err := strconv.Atoi(string(value["host_id"]))
if err != nil {
return err
}
taskHostModel.TaskId = taskId
taskHostModel.HostId = int16(hostId)
_, err = session.Insert(taskHostModel)
if err != nil {
return err
}
}
// 删除task表host_id字段
_, err = session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN host_id", tableName))
logger.Info("已升级到v1.1\n")
return err
}
// 升级到v1.2版本
func (migration *Migration) upgradeFor120(session *xorm.Session) error {
// host表增加cert_file字段
tableName := TablePrefix + "host"
_, err := session.Exec(fmt.Sprintf("ALTER TABLE %s Add COLUMN cert_file VARCHAR(64) NOT NULL DEFAULT ''", tableName))
if err != nil {
return err
}
_, err = session.Exec(fmt.Sprintf("ALTER TABLE %s Add COLUMN token VARCHAR(64) NOT NULL DEFAULT ''", tableName))
return err
}
+12 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/app"
"strconv"
"time"
)
type Status int8
@@ -53,8 +54,8 @@ func (model *BaseModel) parsePageAndPageSize(params CommonMap) {
if model.Page <= 0 {
model.Page = Page
}
if model.PageSize <= 0 || model.PageSize > MaxPageSize {
model.PageSize = PageSize
if model.PageSize <= 0 {
model.PageSize = MaxPageSize
}
}
@@ -93,6 +94,7 @@ func CreateDb() *xorm.Engine {
engine.Logger().SetLevel(core.LOG_DEBUG)
}
go keepDbAlived(engine)
return engine
}
@@ -138,4 +140,12 @@ func getDbConfig() map[string]string {
db["max_open_conns"] = app.Setting.Key("db.max.open.conns").String()
return db
}
func keepDbAlived(engine *xorm.Engine) {
t := time.Tick(180 * time.Second)
for {
<- t
engine.Ping()
}
}
+113 -44
View File
@@ -4,6 +4,7 @@ import (
"time"
"github.com/go-xorm/xorm"
"errors"
"strings"
)
type TaskProtocol int8
@@ -13,36 +14,46 @@ const (
TaskRPC // RPC方式执行命令
)
type TaskLevel int8
const (
TaskLevelParent TaskLevel = 1 // 父任务
TaskLevelChild TaskLevel = 2 // 子任务(依赖任务)
)
type TaskDependencyStatus int8
const (
TaskDependencyStatusStrong TaskDependencyStatus = 1 // 强依赖
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
)
// 任务
type Task struct {
Id int `xorm:"int pk autoincr"`
Name string `xorm:"varchar(32) notnull"` // 任务名称
Level TaskLevel `xorm:"smallint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
DependencyStatus TaskDependencyStatus `xorm:"smallint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:系统命令
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:系统命令
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
HostId int16 `xorm:"smallint notnull default 0"` // RPC host id
NotifyStatus int8 `xorm:"smallint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
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:停止
Status Status `xorm:"tinyint notnull index default 0"` // 状态 1:正常 0:停止
Created time.Time `xorm:"datetime notnull created"` // 创建时间
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
BaseModel `xorm:"-"`
Hosts []TaskHostDetail `xorm:"-"`
}
type TaskHost struct {
Task `xorm:"extends"`
Name string
Port int
Alias string
}
func (TaskHost) TableName() string {
return TablePrefix + "task"
func taskHostTableName() []string {
return []string{TablePrefix + "task_host", "th"}
}
// 新增
@@ -59,6 +70,7 @@ func (task *Task) Create() (insertId int, err error) {
func (task *Task) CreateTestTask() {
// HTTP任务
task.Name = "测试HTTP任务"
task.Level = TaskLevelParent
task.Protocol = TaskHTTP
task.Spec = "*/30 * * * * *"
// 查询IP地址区域信息
@@ -68,7 +80,9 @@ func (task *Task) CreateTestTask() {
}
func (task *Task) UpdateBean(id int) (int64, error) {
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)
return Db.ID(id).
Cols("name,spec,protocol,command,timeout,multi,retry_times,remark,notify_status,notify_type,notify_receiver_id, dependency_task_id, dependency_status").
Update(task)
}
// 更新
@@ -92,28 +106,48 @@ func (task *Task) Enable(id int) (int64, error) {
}
// 获取所有激活任务
func (task *Task) ActiveList() ([]TaskHost, error) {
list := make([]TaskHost, 0)
fields := "t.*, host.alias,host.name,host.port"
err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.status = ?", Enabled).Cols(fields).Find(&list)
func (task *Task) ActiveList() ([]Task, error) {
list := make([]Task, 0)
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).
Find(&list)
return list, err
if err != nil {
return list, err
}
return task.setHostsForTasks(list)
}
// 获取某个主机下的所有激活任务
func (task *Task) ActiveListByHostId(hostId int16) ([]TaskHost, error) {
list := make([]TaskHost, 0)
fields := "t.*, host.alias,host.name,host.port"
err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.status = ? AND t.host_id = ?", Enabled, hostId).Cols(fields).Find(&list)
func (task *Task) ActiveListByHostId(hostId int16) ([]Task, error) {
taskHostModel := new(TaskHost)
taskIds, err := taskHostModel.GetTaskIdsByHostId(hostId)
if err != nil {
return nil, err
}
list := make([]Task, 0)
err = Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).
In("id", taskIds...).
Find(&list)
if err != nil {
return list, err
}
return list, err
return task.setHostsForTasks(list)
}
// 判断主机id是否有引用
func (task *Task) HostIdExist(hostId int16) (bool, error) {
count, err := Db.Where("host_id = ?", hostId).Count(task);
func (task *Task) setHostsForTasks(tasks []Task) ([]Task, error) {
taskHostModel := new(TaskHost)
var err error
for i, value := range tasks {
taskHostDetails, err := taskHostModel.GetHostIdsByTaskId(value.Id)
if err != nil {
return nil, err
}
tasks[i].Hosts = taskHostDetails
}
return count > 0, err
return tasks, err
}
// 判断任务名称是否存在
@@ -139,29 +173,67 @@ func (task *Task) GetStatus(id int) (Status, error) {
return task.Status, nil
}
func(task *Task) Detail(id int) (TaskHost, error) {
taskHost := TaskHost{}
fields := "t.*, host.alias,host.name,host.port"
_, err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.id=?", id).Cols(fields).Get(&taskHost)
func(task *Task) Detail(id int) (Task, error) {
t := Task{}
_, err := Db.Where("id=?", id).Get(&t)
return taskHost, err
if err != nil {
return t, err
}
taskHostModel := new(TaskHost)
t.Hosts, err = taskHostModel.GetHostIdsByTaskId(id)
return t, err
}
func (task *Task) List(params CommonMap) ([]TaskHost, error) {
func (task *Task) List(params CommonMap) ([]Task, error) {
task.parsePageAndPageSize(params)
list := make([]TaskHost, 0)
fields := "t.*, host.alias,host.name"
session := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id")
list := make([]Task, 0)
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
task.parseWhere(session, params)
err := session.Cols(fields).Desc("t.id").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
err := session.GroupBy("t.id").Desc("t.id").Cols("t.*").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
return list, err
if err != nil {
return nil, err
}
return task.setHostsForTasks(list)
}
// 获取依赖任务列表
func (task *Task) GetDependencyTaskList(ids string) ([]Task, error) {
list := make([]Task, 0)
if ids == "" {
return list, nil
}
idList := strings.Split(ids, ",")
taskIds := make([]interface{}, len(idList))
for i, v := range idList {
taskIds[i] = v
}
fields := "t.*"
err := Db.Alias("t").
Where("t.level = ?", TaskLevelChild).
In("t.id", taskIds).
Cols(fields).
Find(&list)
if err != nil {
return list, err
}
return task.setHostsForTasks(list)
}
func (task *Task) Total(params CommonMap) (int64, error) {
session := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id")
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
task.parseWhere(session, params)
return session.Count(task)
list := make([]Task, 0)
err := session.GroupBy("t.id").Find(&list)
return int64(len(list)), err
}
// 解析where
@@ -175,7 +247,7 @@ func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
}
hostId, ok := params["HostId"]
if ok && hostId.(int) > 0 {
session.And("host_id = ?", hostId)
session.And("th.host_id = ?", hostId)
}
name, ok := params["Name"]
if ok && name.(string) != "" {
@@ -191,6 +263,3 @@ func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
}
}
func hostTableName() []string {
return []string{TablePrefix + "host", "host"}
}
+83
View File
@@ -0,0 +1,83 @@
package models
type TaskHost struct {
Id int `xorm:"int pk autoincr"`
TaskId int `xorm:"int not null index"`
HostId int16 `xorm:"smallint not null index"`
}
type TaskHostDetail struct {
TaskHost `xorm:"extends"`
Name string
Port int
Alias string
CertFile string
Token string
}
func (TaskHostDetail) TableName() string {
return TablePrefix + "task_host"
}
func hostTableName() []string {
return []string{TablePrefix + "host", "h"}
}
func (th *TaskHost) Remove(taskId int) error {
_, err := Db.Where("task_id = ?", taskId).Delete(new(TaskHost))
return err
}
func (th *TaskHost) Add(taskId int, hostIds []int) error {
err := th.Remove(taskId)
if err != nil {
return err
}
taskHosts := make([]TaskHost, len(hostIds))
for i, value := range hostIds {
taskHosts[i].TaskId = taskId
taskHosts[i].HostId = int16(value)
}
_, err = Db.Insert(&taskHosts)
return err
}
func (th *TaskHost) GetHostIdsByTaskId(taskId int) ([]TaskHostDetail, error) {
list := make([]TaskHostDetail, 0)
fields := "th.id,th.host_id,h.alias,h.name,h.port,h.cert_file,h.token"
err := Db.Alias("th").
Join("LEFT", hostTableName(), "th.host_id=h.id").
Where("th.task_id = ?", taskId).
Cols(fields).
Find(&list)
return list, err
}
func (th *TaskHost) GetTaskIdsByHostId(hostId int16) ([]interface{}, error) {
list := make([]TaskHost, 0)
err := Db.Where("host_id = ?", hostId).Cols("task_id").Find(&list)
if err != nil {
return nil, err
}
taskIds := make([]interface{}, len(list))
for i, value := range list {
taskIds[i] = value.TaskId
}
return taskIds, err
}
// 判断主机id是否有引用
func (th *TaskHost) HostIdExist(hostId int16) (bool, error) {
count, err := Db.Where("host_id = ?", hostId).Count(th);
return count > 0, err
}
+53 -33
View File
@@ -4,11 +4,11 @@ import (
"os"
"github.com/ouqiang/gocron/modules/logger"
"runtime"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/ini.v1"
"io/ioutil"
"strconv"
"strings"
)
var (
@@ -19,12 +19,12 @@ var (
AppConfig string // 应用配置文件
Installed bool // 应用是否安装过
Setting *ini.Section // 应用配置
PidFile string
VersionId int // 版本号
VersionFile string // 版本号文件
)
func InitEnv() {
runtime.GOMAXPROCS(runtime.NumCPU())
func InitEnv(versionString string) {
logger.InitLogger()
wd, err := os.Getwd()
if err != nil {
@@ -35,12 +35,13 @@ func InitEnv() {
LogDir = AppDir + "/log"
DataDir = AppDir + "/data"
AppConfig = ConfDir + "/app.ini"
PidFile = LogDir + "/gocron.pid"
VersionFile = ConfDir + "/.version"
checkDirExists(ConfDir, LogDir, DataDir)
Installed = IsInstalled()
VersionId = ToNumberVersion(versionString)
}
// 判断应用是否安装
// 判断应用是否安装
func IsInstalled() bool {
_, err := os.Stat(ConfDir + "/install.lock")
if os.IsNotExist(err) {
@@ -50,33 +51,6 @@ func IsInstalled() bool {
return true
}
func WritePid() {
pid := os.Getpid()
pidStr := strconv.Itoa(pid)
err := ioutil.WriteFile(PidFile, []byte(pidStr), 0644)
if err != nil {
logger.Fatal("写入pid文件失败", err)
}
}
func GetPid() int {
bytes, err := ioutil.ReadFile(PidFile)
if err != nil {
return 0
}
pidStr := string(bytes)
pid, err := strconv.Atoi(pidStr)
if err != nil {
return 0
}
return pid
}
func RemovePid() {
os.Remove(PidFile)
}
// 创建安装锁文件
func CreateInstallLock() error {
_, err := os.Create(ConfDir + "/install.lock")
@@ -87,6 +61,52 @@ func CreateInstallLock() error {
return err
}
// 更新应用版本号文件
func UpdateVersionFile() {
err := ioutil.WriteFile(VersionFile,
[]byte(strconv.Itoa(VersionId)),
0644,
)
if err != nil {
logger.Fatal(err)
}
}
// 获取应用当前版本号, 从版本号文件中读取
func GetCurrentVersionId() int {
if !utils.FileExist(VersionFile) {
return 0;
}
bytes, err := ioutil.ReadFile(VersionFile)
if err != nil {
logger.Fatal(err)
}
versionId, err := strconv.Atoi(strings.TrimSpace(string(bytes)))
if err != nil {
logger.Fatal(err)
}
return versionId
}
// 把字符串版本号a.b.c转换为整数版本号abc
func ToNumberVersion(versionString string) int {
v := strings.Replace(versionString, ".", "", -1)
if len(v) < 3 {
v += "0"
}
versionId, err := strconv.Atoi(v)
if err != nil {
logger.Fatal(err)
}
return versionId
}
// 检测目录是否存在
func checkDirExists(path ...string) {
for _, value := range path {
+7 -7
View File
@@ -16,11 +16,11 @@ var (
errUnavailable = errors.New("无法连接远程服务器")
)
func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
tryTimes := 60
func ExecWithRetry(ip string, port int, certFile string, token string, taskReq *pb.TaskRequest) (string, error) {
tryTimes := 15
i := 0
for i < tryTimes {
output, err := exec(ip, port, taskReq)
output, err := Exec(ip, port, certFile, token, taskReq)
if err != errUnavailable {
return output, err
}
@@ -31,14 +31,14 @@ func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
return "", errUnavailable
}
func exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
func Exec(ip string, port int, certFile string, token string, taskReq *pb.TaskRequest) (string, error) {
defer func() {
if err := recover(); err != nil {
logger.Error("panic#rpc/client.go:Exec#", err)
}
} ()
addr := fmt.Sprintf("%s:%d", ip, port)
conn, err := grpcpool.Pool.Get(addr)
conn, err := grpcpool.Pool.Get(addr, certFile, token)
if err != nil {
return "", err
}
@@ -69,7 +69,7 @@ func exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string, error) {
switch grpc.Code(err) {
case codes.Unavailable:
case codes.Unavailable, codes.Internal:
conn.Close()
*connClosed = true
return "", errUnavailable
@@ -77,4 +77,4 @@ func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string,
return "", errors.New("执行超时, 强制结束")
}
return "", err
}
}
+41 -5
View File
@@ -6,6 +6,9 @@ import (
"time"
"google.golang.org/grpc"
"errors"
"google.golang.org/grpc/credentials"
"golang.org/x/net/context"
"strings"
)
@@ -30,18 +33,20 @@ type GRPCPool struct {
sync.RWMutex
}
func (p *GRPCPool) Get(addr string) (*grpc.ClientConn, error) {
func (p *GRPCPool) Get(addr, certFile, token string) (*grpc.ClientConn, error) {
p.RLock()
p.RUnlock()
pool, ok := p.conns[addr]
p.RUnlock()
if !ok {
err := p.newCommonPool(addr)
err := p.newCommonPool(addr, certFile, token)
if err != nil {
return nil, err
}
}
p.RLock()
pool = p.conns[addr]
p.RUnlock()
conn, err := pool.Get()
if err != nil {
return nil, err
@@ -84,7 +89,7 @@ func (p *GRPCPool) ReleaseAll() {
}
// 初始化底层连接池
func (p *GRPCPool) newCommonPool(addr string) (error) {
func (p *GRPCPool) newCommonPool(addr, certFile, token string) (error) {
p.Lock()
defer p.Unlock()
commonPool, ok := p.conns[addr]
@@ -95,7 +100,23 @@ func (p *GRPCPool) newCommonPool(addr string) (error) {
InitialCap: 1,
MaxCap: 30,
Factory: func() (interface{}, error) {
return grpc.Dial(addr, grpc.WithInsecure())
if certFile == "" {
return grpc.Dial(addr, grpc.WithInsecure())
}
server := strings.Split(addr, ":")
creds, err := credentials.NewClientTLSFromFile(certFile, server[0])
if err != nil {
return nil, err
}
customCredential := &CustomCredential{Token: token}
return grpc.Dial(addr,
grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(customCredential),
)
},
Close: func(v interface{}) error {
conn, ok := v.(*grpc.ClientConn)
@@ -115,4 +136,19 @@ func (p *GRPCPool) newCommonPool(addr string) (error) {
p.conns[addr] = commonPool
return nil
}
type CustomCredential struct
{
Token string
}
func (c CustomCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"token": c.Token,
}, nil
}
func (c CustomCredential) RequireTransportSecurity() bool {
return true
}
+58 -5
View File
@@ -7,9 +7,33 @@ import (
"google.golang.org/grpc"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"github.com/ouqiang/gocron/modules/utils"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"errors"
)
type Server struct {}
type Server struct
{
Token string
}
func (s Server) auth(ctx context.Context) error {
// 验证token是否有效
meta, ok := metadata.FromContext(ctx)
if !ok {
return errors.New("missing metadata")
}
token, ok := meta["token"]
if !ok {
return errors.New("missing param token")
}
if token[0] != s.Token {
return errors.New("invalid token")
}
return nil
}
func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
defer func() {
@@ -17,6 +41,15 @@ func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse,
grpclog.Println(err)
}
} ()
if s.Token != "" {
err := s.auth(ctx)
if err != nil {
return nil, err
}
}
output, err := utils.ExecShell(ctx, req.Command)
resp := new(pb.TaskResponse)
resp.Output = output
@@ -29,14 +62,34 @@ func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse,
return resp, nil
}
func Start(addr string) {
func Start(addr, certFile, keyFile, token string) {
defer func() {
if err := recover(); err != nil {
grpclog.Println("panic", err)
}
} ()
l, err := net.Listen("tcp", addr)
if err != nil {
grpclog.Fatal(err)
}
s := grpc.NewServer()
pb.RegisterTaskServer(s, Server{})
grpclog.Println("listen address ", addr)
var s *grpc.Server
server := Server{Token: token}
if certFile != "" {
// TLS认证
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
s = grpc.NewServer(grpc.Creds(creds))
pb.RegisterTaskServer(s, server)
grpclog.Printf("listen %s with TLS", addr)
} else {
s = grpc.NewServer()
pb.RegisterTaskServer(s, server)
grpclog.Println("listen ", addr)
}
err = s.Serve(l)
if err != nil {
grpclog.Fatal(err)
+1
View File
@@ -20,6 +20,7 @@ const ResponseFailure = 1
const NotFound = 2
const AuthError = 3
const ServerError = 4
const CaptchaError = 5
const SuccessContent = "操作成功"
const FailureContent = "操作失败"
+8 -5
View File
@@ -10,13 +10,16 @@ function Util() {
swal("操作成功", '保存成功', 'success');
};
// ajax成功处理
util.ajaxSuccess = function(response, callback) {
util.ajaxSuccess = function(response, callback, failureCallback) {
if (response.code === undefined) {
swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error');
return;
}
if (response.code != SUCCESS) {
swal(FAILURE_MESSAGE, response.message ,'error');
if (failureCallback !== undefined) {
failureCallback(response.code, response.message);
}
return;
}
if (callback !== undefined) {
@@ -26,7 +29,7 @@ function Util() {
// ajax错误处理
util.ajaxFailure = function() {
// todo 错误处理
swal(FAILURE_MESSAGE, '未知错误', 'error');
swal(FAILURE_MESSAGE, '操作失败', 'error');
};
// get请求
util.get = function(url, callback) {
@@ -39,12 +42,12 @@ function Util() {
).error(util.ajaxFailure);
};
// post请求
util.post = function(url, params, callback) {
util.post = function(url, params, callback, failureCallback) {
$.post(
url,
util.objectTrim(params),
function(response) {
util.ajaxSuccess(response, callback);
util.ajaxSuccess(response, callback, failureCallback);
},
'json'
).error(util.ajaxFailure);
@@ -57,7 +60,7 @@ function Util() {
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
confirmButtonText: '删除',
confirmButtonText: '确定',
cancelButtonColor: '#d33',
cancelButtonText: "取消",
closeOnConfirm: false,
-110
View File
@@ -1,110 +0,0 @@
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
}
+41 -7
View File
@@ -14,6 +14,8 @@ import (
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
"strings"
"github.com/ouqiang/gocron/modules/rpc/client"
"github.com/ouqiang/gocron/modules/rpc/proto"
)
func Index(ctx *macaron.Context) {
@@ -62,6 +64,8 @@ type HostForm struct {
Name string `binding:"Required;MaxSize(64)"`
Alias string `binding:"Required;MaxSize(32)"`
Port int `binding:"Required;Range(1-65535)"`
CertFile string
Token string
Remark string
}
@@ -91,6 +95,13 @@ func Store(ctx *macaron.Context, form HostForm) string {
hostModel.Alias = strings.TrimSpace(form.Alias)
hostModel.Port = form.Port
hostModel.Remark = strings.TrimSpace(form.Remark)
hostModel.CertFile = strings.TrimSpace(form.CertFile)
hostModel.Token = strings.TrimSpace(form.Token)
if hostModel.CertFile != "" && !utils.FileExist(hostModel.CertFile) {
return json.CommonFailure("证书文件不存在或无权限访问")
}
isCreate := false
oldHostModel := new(models.Host)
err = oldHostModel.Find(int(id))
@@ -98,6 +109,7 @@ func Store(ctx *macaron.Context, form HostForm) string {
return json.CommonFailure("主机不存在")
}
if id > 0 {
_, err = hostModel.UpdateBean(id)
} else {
@@ -110,12 +122,9 @@ func Store(ctx *macaron.Context, form HostForm) string {
if !isCreate {
oldAddr := fmt.Sprintf("%s:%d", oldHostModel.Name, oldHostModel.Port)
newAddr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
if oldAddr != newAddr {
grpcpool.Pool.Release(oldAddr)
}
grpcpool.Pool.Release(oldAddr)
taskModel := new(models.TaskHost)
taskModel := new(models.Task)
tasks, err := taskModel.ActiveListByHostId(id)
if err != nil {
return json.CommonFailure("刷新任务主机信息失败", err)
@@ -133,8 +142,8 @@ func Remove(ctx *macaron.Context) string {
if err != nil {
return json.CommonFailure("参数错误", err)
}
taskModel := new(models.Task)
exist,err := taskModel.HostIdExist(int16(id))
taskHostModel := new(models.TaskHost)
exist,err := taskHostModel.HostIdExist(int16(id))
if err != nil {
return json.CommonFailure("操作失败", err)
}
@@ -160,6 +169,31 @@ func Remove(ctx *macaron.Context) string {
return json.Success("操作成功", nil)
}
func Ping(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
hostModel := new(models.Host)
err := hostModel.Find(id)
json := utils.JsonResponse{}
if err != nil || hostModel.Id <= 0{
return json.CommonFailure("主机不存在", err)
}
taskReq := &rpc.TaskRequest{}
taskReq.Command = "echo hello"
taskReq.Timeout = 10
output, err := client.Exec(hostModel.Name,
hostModel.Port,
hostModel.CertFile,
hostModel.Token,
taskReq)
if err != nil {
return json.CommonFailure("连接失败-" + err.Error() + " " + output, err)
}
return json.Success("连接成功", nil)
}
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
var params models.CommonMap = models.CommonMap{}
+4 -4
View File
@@ -75,7 +75,7 @@ func Store(ctx *macaron.Context, form InstallForm) string {
models.Db = models.CreateDb()
// 创建数据库表
migration := new(models.Migration)
err = migration.Exec(form.DbName)
err = migration.Install(form.DbName)
if err != nil {
return json.CommonFailure(fmt.Sprintf("创建数据库表失败-%s", err.Error()), err)
}
@@ -92,6 +92,9 @@ func Store(ctx *macaron.Context, form InstallForm) string {
return json.CommonFailure("创建文件安装锁失败", err)
}
// 更新版本号文件
app.UpdateVersionFile()
app.Installed = true
// 初始化定时任务
serviceTask := new(service.Task)
@@ -115,9 +118,6 @@ func writeConfig(form InstallForm) error {
"db.max.open.conns", "100",
"allow_ips", "",
"app.name", "定时任务管理系统", // 应用名称
"delay.task.enable", "false", // 是否开启延时任务
"delay.task.slots", "3600", // 时间轮槽数量
"delay.task.tick", "1s", // 时间轮每次转动的时间
"api.key", "",
"api.secret", "",
}
+16 -8
View File
@@ -17,9 +17,11 @@ import (
"github.com/go-macaron/gzip"
"github.com/ouqiang/gocron/routers/manage"
"github.com/ouqiang/gocron/routers/loginlog"
"github.com/ouqiang/gocron/routers/delaytask"
"time"
"strconv"
"html/template"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
)
// 静态文件目录
@@ -60,17 +62,13 @@ 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)
m.Get("/edit/:id", host.Edit)
m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
m.Get("", host.Index)
m.Get("/ping/:id", host.Ping)
m.Post("/remove/:id", host.Remove)
})
@@ -97,8 +95,8 @@ func Register(m *macaron.Macaron) {
// API
m.Group("/api/v1", func() {
m.Post("/tasklog/remove/:id", tasklog.Remove)
m.Post("/delaytask/push", delaytask.Create)
m.Post("/delaytask/log/remove/:id", delaytask.Remove)
m.Post("/task/enable/:id", task.Enable)
m.Post("/task/disable/:id", task.Disable)
}, apiAuth);
// 404错误
@@ -143,7 +141,17 @@ func RegisterMiddleware(m *macaron.Macaron) {
IndentJSON: true,
// 渲染具有缩进格式的 XML,默认为不缩进
IndentXML: true,
Funcs: []template.FuncMap{map[string]interface{} {
"HostFormat": func(index int) bool {
return (index + 1) % 3 == 0
},
"unescape": func(str string) template.HTML {
return template.HTML(str)
},
}},
}))
m.Use(cache.Cacher())
m.Use(captcha.Captchaer())
m.Use(session.Sessioner(session.Options{
Provider: "file",
ProviderConfig: app.DataDir + "/sessions",
+72 -15
View File
@@ -18,14 +18,17 @@ import (
type TaskForm struct {
Id int
Level models.TaskLevel `binding:"Required;In(1,2)"`
DependencyStatus models.TaskDependencyStatus
DependencyTaskId string
Name string `binding:"Required;MaxSize(32)"`
Spec string `binding:"Required;MaxSize(64)"`
Spec string
Protocol models.TaskProtocol `binding:"In(1,2)"`
Command string `binding:"Required;MaxSize(256)"`
Timeout int `binding:"Range(0,86400)"`
Multi int8 `binding:"In(1,2)"`
RetryTimes int8
HostId int16
HostId string
Remark string
NotifyStatus int8 `binding:"In(1,2,3)"`
NotifyType int8 `binding:"In(1,2,3)"`
@@ -88,9 +91,22 @@ func Edit(ctx *macaron.Context) {
logger.Errorf("编辑任务#获取任务详情失败#任务ID-%d#%s", id, err.Error())
ctx.Redirect("/task")
}
hostModel := new(models.Host)
hostModel.PageSize = -1
hosts, err := hostModel.List(models.CommonMap{})
if err != nil {
logger.Error(err)
} else {
for i, host := range(hosts) {
if inHosts(task.Hosts, host.Id) {
hosts[i].Selected = true
}
}
}
ctx.Data["Task"] = task
ctx.Data["Hosts"] = hosts
ctx.Data["Title"] = "编辑"
setHostsToTemplate(ctx)
ctx.HTML(200, "task/task_form")
}
@@ -99,10 +115,6 @@ func Store(ctx *macaron.Context, form TaskForm) string {
json := utils.JsonResponse{}
taskModel := models.Task{}
var id int = form.Id
_, err := cron.Parse(form.Spec)
if err != nil {
return json.CommonFailure("crontab表达式解析失败", err)
}
nameExists, err := taskModel.NameExist(form.Name, form.Id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
@@ -111,15 +123,10 @@ func Store(ctx *macaron.Context, form TaskForm) string {
return json.CommonFailure("任务名称已存在")
}
if form.Protocol == models.TaskRPC && form.HostId <= 0 {
if form.Protocol == models.TaskRPC && form.HostId == "" {
return json.CommonFailure("请选择主机名")
}
if form.Protocol == models.TaskRPC {
taskModel.HostId = form.HostId
} else {
taskModel.HostId = 0
}
taskModel.Name = form.Name
taskModel.Protocol = form.Protocol
taskModel.Command = form.Command
@@ -134,8 +141,11 @@ func Store(ctx *macaron.Context, form TaskForm) string {
taskModel.NotifyType = form.NotifyType - 1
taskModel.NotifyReceiverId = form.NotifyReceiverId
taskModel.Spec = form.Spec
taskModel.Level = form.Level
taskModel.DependencyStatus = form.DependencyStatus
taskModel.DependencyTaskId = strings.TrimSpace(form.DependencyTaskId)
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
return json.CommonFailure("至少选择一个接收者")
return json.CommonFailure("至少选择一个通知接收者")
}
if taskModel.Protocol == models.TaskHTTP {
command := strings.ToLower(taskModel.Command)
@@ -151,6 +161,27 @@ func Store(ctx *macaron.Context, form TaskForm) string {
return json.CommonFailure("任务重试次数取值0-10")
}
if (taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
taskModel.DependencyStatus != models.TaskDependencyStatusWeak) {
return json.CommonFailure("请选择依赖关系")
}
if taskModel.Level == models.TaskLevelParent {
_, err = cron.Parse(form.Spec)
if err != nil {
return json.CommonFailure("crontab表达式解析失败", err)
}
} else {
taskModel.DependencyTaskId = ""
taskModel.Spec = ""
}
if id > 0 && taskModel.DependencyTaskId != "" {
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
return json.CommonFailure("不允许设置当前任务为子任务")
}
}
if id == 0 {
// 任务添加后开始调度执行
@@ -164,8 +195,20 @@ func Store(ctx *macaron.Context, form TaskForm) string {
return json.CommonFailure("保存失败", err)
}
taskHostModel := new(models.TaskHost)
if form.Protocol == models.TaskRPC {
hostIdStrList := strings.Split(form.HostId, ",")
hostIds := make([]int, len(hostIdStrList))
for i, hostIdStr := range hostIdStrList {
hostIds[i], _ = strconv.Atoi(hostIdStr)
}
taskHostModel.Add(id, hostIds)
} else {
taskHostModel.Remove(id)
}
status, err := taskModel.GetStatus(id)
if status == models.Enabled {
if status == models.Enabled && taskModel.Level == models.TaskLevelParent {
addTaskToTimer(id)
}
@@ -182,6 +225,9 @@ func Remove(ctx *macaron.Context) string {
return json.CommonFailure(utils.FailureContent, err)
}
taskHostModel := new(models.TaskHost)
taskHostModel.Remove(id)
service.Cron.RemoveJob(strconv.Itoa(id))
return json.Success(utils.SuccessContent, nil)
@@ -267,9 +313,20 @@ func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
func setHostsToTemplate(ctx *macaron.Context) {
hostModel := new(models.Host)
hostModel.PageSize = -1
hosts, err := hostModel.List(models.CommonMap{})
if err != nil {
logger.Error(err)
}
ctx.Data["Hosts"] = hosts
}
func inHosts(slice []models.TaskHostDetail, element int16) bool {
for _, v := range slice {
if v.HostId == element {
return true
}
}
return false
}
+5 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/ouqiang/gocron/models"
"github.com/go-macaron/session"
"github.com/ouqiang/gocron/modules/logger"
"github.com/go-macaron/captcha"
)
// @author qiang.ou<qingqianludao@gmail.com>
@@ -47,7 +48,7 @@ func UpdatePassword(ctx *macaron.Context, sess session.Store) string {
return json.Success("修改成功", nil)
}
func ValidateLogin(ctx *macaron.Context, sess session.Store) string {
func ValidateLogin(ctx *macaron.Context, sess session.Store, cpt *captcha.Captcha) string {
username := ctx.QueryTrim("username")
password := ctx.QueryTrim("password")
json := utils.JsonResponse{}
@@ -58,6 +59,9 @@ func ValidateLogin(ctx *macaron.Context, sess session.Store) string {
if !userModel.Match(username, password) {
return json.CommonFailure("用户名或密码错误")
}
if !cpt.VerifyReq(ctx.Req) {
return json.Failure(utils.CaptchaError, "验证码错误")
}
loginLogModel := new(models.LoginLog)
loginLogModel.Username = userModel.Name
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

-133
View File
@@ -1,133 +0,0 @@
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"
"fmt"
"github.com/ouqiang/gocron/modules/app"
)
var tw *timewheel.TimeWheel
type DelayTask struct {}
// 从数据库中取出所有延迟任务
func (task *DelayTask) Initialize(tick time.Duration, slots int) {
tw = timewheel.New(tick, slots, task.Run)
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)
// 时间过期, 立即执行任务
data := []interface{}{taskModel.Id, taskModel.Url, taskModel.Params}
if execTimestamp <= currentTimestamp {
go task.Run(data)
return
}
delay := execTimestamp - currentTimestamp
tw.Add(time.Duration(delay) * time.Second, data)
}
// 运行任务
func (task *DelayTask) Run(data []interface{}) {
if len(data) < 3 {
logger.Errorf("延时任务开始执行#参数不足#%+v", data)
return
}
id := data[0].(int64)
url := data[1].(string)
params := data[2].(string)
if id <= 0 || url == "" {
logger.Errorf("延时任务开始执行#参数为空#%+v", data)
return
}
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 {
msg := fmt.Sprintf("延迟任务执行失败#重试第%d次#任务Id-%d#HTTP状态码-%d#HTTP-BODY-%s",
i,id,response.StatusCode,response.Body)
logger.Error(msg)
FailureNotify(msg)
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()
}
func FailureNotify(message string) {
notifyUrl := app.Setting.Key("delay.task.failure.notify.url").String()
notifyUrl = strings.TrimSpace(notifyUrl)
if notifyUrl != "" {
params := fmt.Sprintf("error=%s", message)
httpclient.PostParams(notifyUrl, params, 60)
}
}
+139 -23
View File
@@ -13,6 +13,10 @@ import (
"sync"
rpcClient "github.com/ouqiang/gocron/modules/rpc/client"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"strings"
"text/template"
"bytes"
"encoding/base64"
)
// 定时任务调度管理器
@@ -47,13 +51,16 @@ func (c *TaskCount) Num() int {
return c.num
}
// 任务ID作为Key, 不会出现并发写, 不加锁
// 任务ID作为Key
type Instance struct {
Status map[int]bool
sync.RWMutex
}
// 是否有任务处于运行中
func (i *Instance) has(key int) bool {
i.RLock()
defer i.RUnlock()
running, ok := i.Status[key]
if ok && running {
return true
@@ -63,11 +70,15 @@ func (i *Instance) has(key int) bool {
}
func (i *Instance) add(key int) {
i.Lock()
defer i.Unlock()
i.Status[key] = true
}
func (i *Instance) done(key int) {
i.Status[key] = false
i.Lock()
defer i.Unlock()
delete(i.Status, key)
}
type Task struct{}
@@ -82,7 +93,7 @@ type TaskResult struct {
func (task *Task) Initialize() {
Cron = cron.New()
Cron.Start()
runInstance = Instance{make(map[int]bool)}
runInstance = Instance{make(map[int]bool), sync.RWMutex{}}
TaskNum = TaskCount{0, sync.RWMutex{}}
taskModel := new(models.Task)
@@ -99,14 +110,18 @@ func (task *Task) Initialize() {
}
// 批量添加任务
func (task *Task) BatchAdd(tasks []models.TaskHost) {
func (task *Task) BatchAdd(tasks []models.Task) {
for _, item := range tasks {
task.Add(item)
}
}
// 添加任务
func (task *Task) Add(taskModel models.TaskHost) {
func (task *Task) Add(taskModel models.Task) {
if taskModel.Level == models.TaskLevelChild {
logger.Errorf("添加任务失败#不允许添加子任务到调度器#任务Id-%d", taskModel.Id);
return
}
taskFunc := createJob(taskModel)
if taskFunc == nil {
logger.Error("创建任务处理Job失败,不支持的任务协议#", taskModel.Protocol)
@@ -128,12 +143,12 @@ func (task *Task) StopAll() {
}
// 直接运行任务
func (task *Task) Run(taskModel models.TaskHost) {
func (task *Task) Run(taskModel models.Task) {
go createJob(taskModel)()
}
type Handler interface {
Run(taskModel models.TaskHost) (string, error)
Run(taskModel models.Task) (string, error)
}
@@ -143,7 +158,7 @@ type HTTPHandler struct{}
// http任务执行时间不超过300秒
const HttpExecTimeout = 300
func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error) {
func (h *HTTPHandler) Run(taskModel models.Task) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout
}
@@ -159,26 +174,58 @@ func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error)
// RPC调用执行任务
type RPCHandler struct {}
func (h *RPCHandler) Run(taskModel models.TaskHost) (result string, err error) {
func (h *RPCHandler) Run(taskModel models.Task) (result string, err error) {
taskRequest := new(pb.TaskRequest)
taskRequest.Timeout = int32(taskModel.Timeout)
taskRequest.Command = taskModel.Command
var resultChan chan TaskResult = make(chan TaskResult, len(taskModel.Hosts))
for _, taskHost := range taskModel.Hosts {
go func(th models.TaskHostDetail) {
output, err := rpcClient.ExecWithRetry(th.Name,
th.Port,
th.CertFile,
th.Token,
taskRequest)
var errorMessage string = ""
if err != nil {
errorMessage = err.Error()
}
outputMessage := fmt.Sprintf("主机: [%s-%s]\n%s\n%s\n\n",
th.Alias, th.Name, errorMessage, output,
)
resultChan <- TaskResult{Err:err, Result: outputMessage}
}(taskHost)
}
return rpcClient.Exec(taskModel.Name, taskModel.Port, taskRequest)
var aggregationErr error = nil
var aggregationResult string = ""
for i := 0; i < len(taskModel.Hosts); i++ {
taskResult := <- resultChan
aggregationResult += taskResult.Result
if taskResult.Err != nil {
aggregationErr = taskResult.Err
}
}
return aggregationResult, aggregationErr
}
// 创建任务日志
func createTaskLog(taskModel models.TaskHost, status models.Status) (int64, error) {
func createTaskLog(taskModel models.Task, status models.Status) (int64, error) {
taskLogModel := new(models.TaskLog)
taskLogModel.TaskId = taskModel.Id
taskLogModel.Name = taskModel.Task.Name
taskLogModel.Name = taskModel.Name
taskLogModel.Spec = taskModel.Spec
taskLogModel.Protocol = taskModel.Protocol
taskLogModel.Command = taskModel.Command
taskLogModel.Timeout = taskModel.Timeout
if taskModel.Protocol == models.TaskRPC {
taskLogModel.Hostname = taskModel.Alias + "-" + taskModel.Name
var aggregationHost string = ""
for _, host := range taskModel.Hosts {
aggregationHost += fmt.Sprintf("%s-%s<br>", host.Alias, host.Name)
}
taskLogModel.Hostname = aggregationHost
}
taskLogModel.StartTime = time.Now()
taskLogModel.Status = status
@@ -205,7 +252,7 @@ func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) {
}
func createJob(taskModel models.TaskHost) cron.FuncJob {
func createJob(taskModel models.Task) cron.FuncJob {
var handler Handler = createHandler(taskModel)
if handler == nil {
return nil
@@ -217,16 +264,16 @@ func createJob(taskModel models.TaskHost) cron.FuncJob {
if taskLogId <= 0 {
return
}
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command)
taskResult := execJob(handler, taskModel)
logger.Infof("任务完成#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId)
}
return taskFunc
}
func createHandler(taskModel models.TaskHost) Handler {
func createHandler(taskModel models.Task) Handler {
var handler Handler = nil
switch taskModel.Protocol {
case models.TaskHTTP:
@@ -239,7 +286,8 @@ func createHandler(taskModel models.TaskHost) Handler {
return handler;
}
func beforeExecJob(taskModel models.TaskHost) (taskLogId int64) {
// 任务前置操作
func beforeExecJob(taskModel models.Task) (taskLogId int64) {
if taskModel.Multi == 0 && runInstance.has(taskModel.Id) {
createTaskLog(taskModel, models.Cancel)
return
@@ -258,7 +306,8 @@ func beforeExecJob(taskModel models.TaskHost) (taskLogId int64) {
return taskLogId
}
func afterExecJob(taskModel models.TaskHost, taskResult TaskResult, taskLogId int64) {
// 任务执行后置操作
func afterExecJob(taskModel models.Task, taskResult TaskResult, taskLogId int64) {
if taskResult.Err != nil {
taskResult.Result = taskResult.Err.Error() + "\n" + taskResult.Result
}
@@ -267,11 +316,78 @@ func afterExecJob(taskModel models.TaskHost, taskResult TaskResult, taskLogId in
logger.Error("任务结束#更新任务日志失败-", err)
}
SendNotification(taskModel, taskResult)
// 发送邮件
go SendNotification(taskModel, taskResult)
// 执行依赖任务
go execDependencyTask(taskModel, taskResult)
}
// 执行依赖任务, 多个任务并发执行
func execDependencyTask(taskModel models.Task, taskResult TaskResult) {
// 父任务才能执行子任务
if taskModel.Level != models.TaskLevelParent {
return
}
// 是否存在子任务
dependencyTaskId := strings.TrimSpace(taskModel.DependencyTaskId)
if dependencyTaskId == "" {
return
}
// 父子任务关系为强依赖, 父任务执行失败, 不执行依赖任务
if taskModel.DependencyStatus == models.TaskDependencyStatusStrong && taskResult.Err != nil {
logger.Infof("父子任务为强依赖关系, 父任务执行失败, 不运行依赖任务#主任务ID-%d", taskModel.Id)
return
}
// 获取子任务
model := new(models.Task)
tasks , err := model.GetDependencyTaskList(dependencyTaskId)
if err != nil {
logger.Errorf("获取依赖任务失败#主任务ID-%d#%s", taskModel.Id, err.Error())
return
}
if len(tasks) == 0 {
logger.Errorf("依赖任务列表为空#主任务ID-%d", taskModel.Id)
}
serviceTask := new(Task)
for _, task := range tasks {
task.Command = appendResultToCommand(task.Command, taskResult)
task.Spec = fmt.Sprintf("依赖任务(主任务ID-%d)", taskModel.Id)
serviceTask.Run(task)
}
}
/**
* 添加主任务执行结果到子任务命令中, 占位符{{.Code}} {{.Message}}
*/
func appendResultToCommand(command string, taskResult TaskResult) string {
var code int8 = 0
if taskResult.Err != nil {
code = 1
}
data := map[string]interface{} {
"Code": code,
"Message": base64.StdEncoding.EncodeToString([]byte(taskResult.Result)),
}
var buf *bytes.Buffer = new(bytes.Buffer)
tmpl, err := template.New("command").Parse(command)
if err != nil {
logger.Errorf("替换子任务命令占位符失败#%s", err.Error())
return command
}
err = tmpl.Execute(buf, data)
if err != nil {
logger.Errorf("替换子任务命令占位符失败#%s", err.Error())
return command
}
return buf.String()
}
// 发送任务结果通知
func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
func SendNotification(taskModel models.Task, taskResult TaskResult) {
var statusName string
// 未开启通知
if taskModel.NotifyStatus == 0 {
@@ -293,7 +409,7 @@ func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
msg := notify.Message{
"task_type": taskModel.NotifyType,
"task_receiver_id": taskModel.NotifyReceiverId,
"name": taskModel.Task.Name,
"name": taskModel.Name,
"output": taskResult.Result,
"status": statusName,
"taskId": taskModel.Id,
@@ -302,7 +418,7 @@ func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
}
// 执行具体任务
func execJob(handler Handler, taskModel models.TaskHost) TaskResult {
func execJob(handler Handler, taskModel models.Task) TaskResult {
defer func() {
if err := recover(); err != nil {
logger.Error("panic#service/task.go:execJob#", err)
+1 -2
View File
@@ -61,12 +61,11 @@
<div class="bigcontainer">
<div class="right menu">
<a class="item {{{if or (eq .Controller "task") (eq .Controller "delaytask")}}}active{{{end}}}" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item {{{if eq .Controller "host"}}}active{{{end}}}" href="/host"><i class="linux icon"></i>主机</a>
<a class="item {{{if eq .Controller "host"}}}active{{{end}}}" href="/host"><i class="linux icon"></i>任务节点</a>
<!-- <a class="item {{{if eq .Controller "user"}}}active{{{end}}}" href="/user"><i class="user icon"></i>账户</a> -->
{{{if gt .LoginUid 0}}}
<a class="item {{{if eq .Controller "manage"}}}active{{{end}}}" href="/manage/slack/edit"><i class="settings icon"></i>管理</a>
{{{end}}}
<a class="item" href="https://github.com/ouqiang/gocron/wiki" target="_blank"><i class="file text icon"></i>查看文档</a>
</div>
</div>
</div>
+21 -3
View File
@@ -19,7 +19,7 @@
<div class="field">
<label>主机名</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.Host.Name}}}" placeholder="192.168.50.154">
<input type="text" name="name" value="{{{.Host.Name}}}" placeholder="127.0.0.1">
</div>
</div>
<div class="field">
@@ -29,9 +29,27 @@
</div>
</div>
<div class="field">
<label>主机别名 (方便记忆和引用)</label>
<label>节点名称</label>
<div class="ui small input">
<input type="text" name="alias" value="{{{.Host.Alias}}}">
<input type="text" name="alias" value="{{{.Host.Alias}}}"
placeholder="节点名称如web">
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label>证书路径</label>
<div class="ui small input">
<input type="text" name="cert_file" value="{{{.Host.CertFile}}}"
placeholder="data/certs/server.pem">
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label>Token</label>
<div class="ui small input">
<textarea rows="4" name="token" placeholder="gocron-node中配置的token">{{{.Host.Token}}}</textarea>
</div>
</div>
</div>
+18 -2
View File
@@ -10,7 +10,7 @@
<a href="/host/create">
<i class="large add icon"></i>
<div class="content">
添加主机
添加节点
</div>
</a>
</h3>
@@ -29,13 +29,14 @@
</div>
</div>
</form>
<table class="ui striped table">
<table class="ui celled table">
<thead>
<tr>
<th>ID</th>
<th>主机名</th>
<th>别名</th>
<th>端口</th>
<th>证书</th>
<th>备注</th>
<th>操作</th>
</tr>
@@ -47,12 +48,14 @@
<td>{{{.Name}}}</td>
<td>{{{.Alias}}}</td>
<td>{{{.Port}}}</td>
<td>{{{.CertFile}}}</td>
<td>{{{.Remark}}}</td>
<td class="operation">
<a class="ui purple button" href="/host/edit/{{{.Id}}}">编辑</a>
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button><br>
<div style="margin-top: 5px;">
<a class="ui twitter button" href="/task?host_id={{{.Id}}}">查看任务</a>
<button class="ui blue button" @click="ping({{{.Id}}})">连接测试</button>
</div>
</td>
</tr>
@@ -63,4 +66,17 @@
</div>
</div>
<script type="text/javascript">
var Vue = new Vue({
el: '.ui.celled.table',
methods: {
ping: function(id) {
util.get("/host/ping/" + id, function(code, message) {
swal('操作成功', '连接成功', 'success');
})
}
}
});
</script>
{{{ template "common/footer" . }}}
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/host"}}}active teal{{{end}}} item" href="/host">
<i class="linux icon"></i> 主机列表
<i class="linux icon"></i> 节点列表
</a>
</div>
</div>
-6
View File
@@ -90,12 +90,6 @@
$('.ui.form').form(
{
onSuccess: function(event, fields) {
swal({
title: '',
text: "系统安装中.......",
type: 'info',
showConfirmButton: false
});
util.post('/install/store', fields, function(code, message) {
swal('安装成功');
setTimeout(function() {
-84
View File
@@ -1,84 +0,0 @@
{{{ 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" . }}}
+109 -27
View File
@@ -24,7 +24,7 @@
</div>
<div class="field">
<select name="host_id" id="hostId">
<option value="">选择主机</option>
<option value="">选择节点</option>
{{{range $i, $v := .Hosts}}}
<option value="{{{.Id}}}" {{{if eq $.Params.HostId .Id }}} selected {{{end}}} >{{{.Alias}}}-{{{.Name}}}</option>
{{{end}}}
@@ -49,17 +49,30 @@
</div>
</div>
</form>
<table class="ui pink table task-list">
<div class="field">
<select id="batch-operation">
<option value="0">批量操作</option>
<option value="1">激活</option>
<option value="2">停止</option>
<option value="3">删除</option>
</select>
</div>
<br>
<table class="ui celled table task-list">
<thead>
<tr>
<th>
<input type="checkbox" onclick="checkAll(this)" style="width:25px;height: 25px;">
</th>
<th>任务ID</th>
<th>任务名称</th>
<th>任务类型</th>
<th>cron表达式</th>
<th>执行方式</th>
<th>超时时间</th>
<th>重试次数</th>
<th>单实例运行</th>
<th>主机</th>
<th>任务节点</th>
<th>状态</th>
<th>操作</th>
</tr>
@@ -67,22 +80,39 @@
<tbody>
{{{range $i, $v := .Tasks}}}
<tr>
<td>
<input type="checkbox"
class="sub-check"
data-id="{{{.Id}}}"
style="width:25px;height: 25px;">
</td>
<td>{{{.Id}}}</td>
<td>{{{.Task.Name}}}</td>
<td>{{{.Name}}}</td>
<td>{{{if eq .Level 1}}}主任务{{{else}}}子任务{{{end}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SHELL {{{end}}}</td>
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{if gt .Multi 0}}}{{{else}}}{{{end}}}</td>
<td>{{{.Alias}}}-{{{.Name}}}</td>
<td>{{{if eq .Status 1}}}<span style="color: green;">激活</span>{{{else}}}<span style="color: red;">停止<span>{{{end}}}</td>
<td>
{{{range $k, $h := .Hosts}}}
{{{$h.Alias}}}<br>
{{{end}}}
</td>
<td>
{{{if eq .Level 1}}}
{{{if eq .Status 1}}}<span style="color: green;">激活</span>{{{else}}}<span style="color: red;">停止<span>{{{end}}}
{{{end}}}
</td>
<td>
<div class="ui buttons operation">
<a class="ui purple button" href="/task/edit/{{{.Id}}}">编辑</a>
{{{if eq .Status 1}}}
<button class="ui primary button" @click="changeStatus({{{.Id}}},{{{.Status}}})">停止</button>
{{{else}}}
<button class="ui blue button" @click="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
{{{if eq .Level 1}}}
{{{if eq .Status 1}}}
<button class="ui primary button" @click="changeStatus({{{.Id}}},{{{.Status}}})">停止</button>
{{{else}}}
<button class="ui blue button" @click="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
{{{end}}}
{{{end}}}
<button class="ui positive button" @click="remove({{{.Id}}})">删除</button> <br>
<button class="ui twitter button" @click="run({{{.Id}}})">手动运行</button>
@@ -97,29 +127,54 @@
</div>
</div>
<script type="text/javascript">
$('.ui.checkbox').checkbox();
$('#batch-operation').change(function() {
var type = $(this).val();
if (type == 0) {
return;
}
var ids = [];
$('.sub-check:checked').each(function() {
ids.push($(this).data('id'));
});
if (ids.length == 0) {
swal('错误提示', '至少选择一个任务', 'warning');
return;
}
util.confirm("确定要执行此操作吗", function () {
$.ajaxSetup({
async: false
});
switch (type) {
case "1":
for (i in ids) {
changeStatus(ids[i], false, true);
}
break;
case "2":
for (i in ids) {
changeStatus(ids[i], true, true);
}
break;
case "3":
for (i in ids) {
remove(ids[i], true);
}
break;
}
location.reload();
});
});
var vue = new Vue(
{
el: '.task-list',
methods: {
changeStatus: function (id ,status) {
var url = '';
if (status) {
url = '/task/disable';
} else {
url = '/task/enable';
}
url += '/' + id;
util.post(url,{}, function() {
location.reload();
});
},
remove: function(id) {
util.removeConfirm('/task/remove/' + id);
},
changeStatus: changeStatus,
remove: remove,
run: function(id) {
util.get("/task/run/" + id, function(code, message) {
swal('操作成功', message, 'success');
@@ -129,9 +184,36 @@
}
);
function checkAll(ele) {
if ($(ele).is(":checked")) {
$('.sub-check').prop("checked", true);
} else {
$('.sub-check').prop("checked", false);
}
}
function remove(id, stopReload) {
util.post('/task/remove/' + id, {}, function () {
if (stopReload === undefined) {
location.reload();
}
});
}
function changeStatus(id ,status, stopReload) {
var url = '';
if (status) {
url = '/task/disable';
} else {
url = '/task/enable';
}
url += '/' + id;
util.post(url,{}, function() {
if (stopReload === undefined) {
location.reload();
}
});
}
</script>
{{{ template "common/footer" . }}}
+4 -8
View File
@@ -48,15 +48,15 @@
</div>
</div>
</form>
<table class="ui pink table">
<table class="ui celled table">
<thead>
<tr>
<th>任务ID</th>
<th>任务名称</th>
<th>cron表达式</th>
<th>协议</th>
<th>执行方式</th>
<th>重试次数</th>
<th>主机</th>
<th>任务节点</th>
<th>执行时长</th>
<th>状态</th>
<th>执行结果</th>
@@ -70,7 +70,7 @@
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SHELL {{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{.Hostname}}}</td>
<td>{{{unescape .Hostname}}}</td>
<td>
{{{if and (ne .Status 3) (ne .Status 4)}}}
{{{if gt .TotalTime 0}}}{{{.TotalTime}}}{{{else}}}1{{{end}}}<br>
@@ -127,10 +127,6 @@
</script>
<script type="text/javascript">
function showTime(startTime, endTime, status) {
}
function showResult(name, command,result) {
$('.message').html($('#task-result').html());
new Vue(
-3
View File
@@ -7,9 +7,6 @@
<a class="item {{{if eq .URI "/task/log"}}}active teal{{{end}}} " href="/task/log">
<i class="bar chart icon"></i> 定时任务日志
</a>
<a class="item {{{if eq .URI "/delaytask"}}}active teal{{{end}}} " href="/delaytask">
<i class="bar chart icon"></i> 延时任务日志
</a>
</div>
</div>
</div>
+101 -39
View File
@@ -19,19 +19,63 @@
<div class="content">任务名称</div>
</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.Task.Task.Name}}}">
<input type="text" name="name" placeholder="任务名称" value="{{{.Task.Name}}}">
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label>
<div class="content">
crontab表达式
<div class="content">任务类型</div>
<div class="ui message">
主任务可以配置多个子任务, 当主任务执行完成后自动执行子任务<br>
任务类型新增后不能变更
</div>
</label>
<div class="ui small input">
<input type="text" name="spec" value="{{{.Task.Spec}}}" placeholder="秒 分 时 天 月 周"/>
<select name="level" id="level" {{{if .Task}}}disabled="disabled"{{{end}}}>
<option value="1" {{{if .Task}}} {{{if eq .Task.Level 1}}}selected{{{end}}} {{{end}}}>主任务</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.Level 2}}}selected{{{end}}} {{{end}}}>子任务</option>
</select>
</div>
</div>
<div id="parent-task">
<div class="two fields">
<div class="field">
<label>
<div class="content">依赖关系</div>
<div class="ui message">
强依赖: 主任务执行成功才会运行子任务 <br>
弱依赖: 无论主任务执行是否成功都会运行子任务
</div>
</label>
<select name="dependency_status" id="dependency_status">
<option value="1" {{{if .Task}}} {{{if eq .Task.DependencyStatus 1}}}selected{{{end}}} {{{end}}}>强依赖</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.DependencyStatus 2}}}selected{{{end}}} {{{end}}}>弱依赖</option>
</select>
</div>
<div class="field">
<label>
<div class="content">子任务ID</div>
<div class="ui message">
多个任务ID逗号分隔 <br>
子任务并发执行
</div>
</label>
<div class="ui small input">
<input type="text" name="dependency_task_id" placeholder="可选" value="{{{.Task.DependencyTaskId}}}">
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label>
<div class="content">
crontab表达式
</div>
</label>
<div class="ui small input">
<input type="text" name="spec" value="{{{.Task.Spec}}}" placeholder="秒 分 时 天 月 周"/>
</div>
</div>
</div>
</div>
@@ -39,20 +83,23 @@
<div class="field">
<label>执行方式</label>
<select name="protocol" id="protocol">
<option value="2" {{{if .Task}}} {{{if eq .Task.Protocol 2}}}selected{{{end}}} {{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.Protocol 2}}}selected{{{end}}} {{{end}}}
data-validate-type="selectProtocol">SHELL</option>
<option value="1" {{{if .Task}}} {{{if eq .Task.Protocol 1}}}selected{{{end}}} {{{end}}}>HTTP</option>
</select>
</div>
</div>
<div class="three fields" id="hostField">
<div class="fields" id="hostField">
<div class="field">
<label>主机</label>
<select name="host_id" id="hostId">
<option value="">选择主机</option>
<label>选择任务节点</label>
<div id="hostId">
{{{range $i, $v := .Hosts}}}
<option value="{{{.Id}}}" {{{if $.Task}}}{{{if eq $.Task.HostId .Id }}} selected {{{end}}} {{{end}}}>{{{.Alias}}}-{{{.Name}}}</option>
<label>
<input type="checkbox" value="{{{.Id}}}" {{{if $.Task}}}{{{if $v.Selected}}} checked {{{end}}}{{{end}}} style="width:25px;height: 25px;">{{{.Alias}}}-{{{.Name}}}
{{{if (HostFormat $i) }}}<br>{{{end}}}
</label>
{{{end}}}
</select> &nbsp; <a class="ui blue button" href="/host/create" target="_blank">添加主机</a>
</div> &nbsp; <br> <a class="ui blue button" href="/host/create" target="_blank">添加节点</a>
</div>
</div>
@@ -62,19 +109,15 @@
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{{{.Task.Command}}}</textarea>
</div>
</div>
<div class="six fields">
<div class="field">
<label>任务超时时间()</label>
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{{{if .Task}}} {{{.Task.Timeout}}} {{{else}}} 0 {{{end}}}">
</div>
</div>
<div class="six fields">
<div class="field">
<label>任务失败重试次数</label>
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{{{if .Task}}} {{{.Task.RetryTimes}}} {{{else}}} 0 {{{end}}}">
</div>
</div>
<div class="three fields">
<div class="field">
<label>任务超时时间(, 0-86400)</label>
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{{{if .Task}}} {{{.Task.Timeout}}} {{{else}}}0{{{end}}}">
</div>
<div class="field">
<label>任务失败重试次数 (0-10)</label>
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{{{if .Task}}} {{{.Task.RetryTimes}}} {{{else}}}0{{{end}}}">
</div>
<div class="field">
<label>允许多实例同时运行</label>
<select name="multi">
@@ -82,6 +125,7 @@
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}></option>
</select>
</div>
</div>
<div class="three fields">
<div class="field">
@@ -107,7 +151,7 @@
<div class="two fields">
<div class="field">
<label>备注</label>
<textarea rows="5" name="remark">{{{.Task.Remark}}}</textarea>
<textarea rows="5" name="remark" placeholder="任务备注信息">{{{.Task.Remark}}}</textarea>
</div>
</div>
<div class="ui primary submit button">保存</div> <a class="ui button" onclick="location.href='/task';">取消</a>
@@ -144,6 +188,7 @@
<script type="text/javascript">
$(function() {
changeCommandPlaceholder();
changeLevel();
changeProtocol();
showNotify();
});
@@ -153,7 +198,9 @@
changeProtocol();
});
$('#level').change(function() {
changeLevel();
});
$('#task-status').change(function() {
var selected = $(this).val();
@@ -269,6 +316,28 @@
return receivers.join(",");
}
function parseHostId() {
var hostIds = [];
$('#hostId input:checked').each(function () {
hostIds.push($(this).val());
});
return hostIds.join(",");
}
function changeLevel() {
var selected = $('#level').val();
if (selected == 1) {
// 主任务
$('#parent-task').show();
$('#child-task').hide();
} else {
// 子任务
$('#parent-task').hide();
$('#child-task').show();
}
}
var $uiForm = $('.ui.form');
registerSelectFormValidation("selectProtocol", $uiForm, $('#protocol'), 'protocol');
$($uiForm).form(
@@ -279,6 +348,12 @@
return false;
}
fields.notify_receiver_id = parseNotifyReceiver();
fields.host_id = parseHostId();
if (fields.protocol == 2 && fields.host_id == "") {
swal('错误提示', '请选择任务节点');
return false;
}
util.post('/task/store', fields, function(code, message) {
location.href = "/task"
});
@@ -299,19 +374,6 @@
}
]
},
spec: {
identifier : 'spec',
rules: [
{
type : 'empty',
prompt : '请输入crontab格式表达式'
},
{
type : 'maxLength[64]',
prompt : '长度不能超过64'
}
]
},
command: {
identifier : 'command',
rules: [
+19
View File
@@ -13,6 +13,12 @@
<input type="password" name="password" placeholder="密码">
</div>
</div>
<div class="two fields">
<div class="field">
{{{.Captcha.CreateHtml}}}
<input type="text" name="captcha" placeholder="验证码">
</div>
</div>
<button class="ui button primary button" >登录</button>
</form>
</div>
@@ -25,6 +31,10 @@
onSuccess: function(event, fields) {
util.post('/user/login', fields, function(code, message) {
location.href = "/"
}, function (code, message) {
if (code == 5) {
$('.captcha-img').trigger('click');
}
});
return false;
@@ -47,6 +57,15 @@
prompt : '请输入密码'
}
]
},
Captcha: {
identifier : 'captcha',
rules: [
{
type : 'empty',
prompt : '请输入验证码'
}
]
}
},
inline : true
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+20
View File
@@ -0,0 +1,20 @@
# cache [![Build Status](https://travis-ci.org/go-macaron/cache.svg?branch=master)](https://travis-ci.org/go-macaron/cache) [![](http://gocover.io/_badge/github.com/go-macaron/cache)](http://gocover.io/github.com/go-macaron/cache)
Middleware cache provides cache management for [Macaron](https://github.com/go-macaron/macaron). It can use many cache adapters, including memory, file, Redis, Memcache, PostgreSQL, MySQL, Ledis and Nodb.
### Installation
go get github.com/go-macaron/cache
## Getting Help
- [API Reference](https://gowalker.org/github.com/go-macaron/cache)
- [Documentation](http://go-macaron.com/docs/middlewares/cache)
## Credits
This package is a modified version of [beego/cache](https://github.com/astaxie/beego/tree/master/cache).
## License
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package cache is a middleware that provides the cache management of Macaron.
package cache
import (
"fmt"
"gopkg.in/macaron.v1"
)
const _VERSION = "0.3.0"
func Version() string {
return _VERSION
}
// Cache is the interface that operates the cache data.
type Cache interface {
// Put puts value into cache with key and expire time.
Put(key string, val interface{}, timeout int64) error
// Get gets cached value by given key.
Get(key string) interface{}
// Delete deletes cached value by given key.
Delete(key string) error
// Incr increases cached int-type value by given key as a counter.
Incr(key string) error
// Decr decreases cached int-type value by given key as a counter.
Decr(key string) error
// IsExist returns true if cached value exists.
IsExist(key string) bool
// Flush deletes all cached data.
Flush() error
// StartAndGC starts GC routine based on config string settings.
StartAndGC(opt Options) error
}
// Options represents a struct for specifying configuration options for the cache middleware.
type Options struct {
// Name of adapter. Default is "memory".
Adapter string
// Adapter configuration, it's corresponding to adapter.
AdapterConfig string
// GC interval time in seconds. Default is 60.
Interval int
// Occupy entire database. Default is false.
OccupyMode bool
// Configuration section name. Default is "cache".
Section string
}
func prepareOptions(options []Options) Options {
var opt Options
if len(options) > 0 {
opt = options[0]
}
if len(opt.Section) == 0 {
opt.Section = "cache"
}
sec := macaron.Config().Section(opt.Section)
if len(opt.Adapter) == 0 {
opt.Adapter = sec.Key("ADAPTER").MustString("memory")
}
if opt.Interval == 0 {
opt.Interval = sec.Key("INTERVAL").MustInt(60)
}
if len(opt.AdapterConfig) == 0 {
opt.AdapterConfig = sec.Key("ADAPTER_CONFIG").MustString("data/caches")
}
return opt
}
// NewCacher creates and returns a new cacher by given adapter name and configuration.
// It panics when given adapter isn't registered and starts GC automatically.
func NewCacher(name string, opt Options) (Cache, error) {
adapter, ok := adapters[name]
if !ok {
return nil, fmt.Errorf("cache: unknown adapter '%s'(forgot to import?)", name)
}
return adapter, adapter.StartAndGC(opt)
}
// Cacher is a middleware that maps a cache.Cache service into the Macaron handler chain.
// An single variadic cache.Options struct can be optionally provided to configure.
func Cacher(options ...Options) macaron.Handler {
opt := prepareOptions(options)
cache, err := NewCacher(opt.Adapter, opt)
if err != nil {
panic(err)
}
return func(ctx *macaron.Context) {
ctx.Map(cache)
}
}
var adapters = make(map[string]Cache)
// Register registers a adapter.
func Register(name string, adapter Cache) {
if adapter == nil {
panic("cache: cannot register adapter with nil value")
}
if _, dup := adapters[name]; dup {
panic(fmt.Errorf("cache: cannot register adapter '%s' twice", name))
}
adapters[name] = adapter
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sync"
"time"
"github.com/Unknwon/com"
"gopkg.in/macaron.v1"
)
// Item represents a cache item.
type Item struct {
Val interface{}
Created int64
Expire int64
}
func (item *Item) hasExpired() bool {
return item.Expire > 0 &&
(time.Now().Unix()-item.Created) >= item.Expire
}
// FileCacher represents a file cache adapter implementation.
type FileCacher struct {
lock sync.Mutex
rootPath string
interval int // GC interval.
}
// NewFileCacher creates and returns a new file cacher.
func NewFileCacher() *FileCacher {
return &FileCacher{}
}
func (c *FileCacher) filepath(key string) string {
m := md5.Sum([]byte(key))
hash := hex.EncodeToString(m[:])
return filepath.Join(c.rootPath, string(hash[0]), string(hash[1]), hash)
}
// Put puts value into cache with key and expire time.
// If expired is 0, it will be deleted by next GC operation.
func (c *FileCacher) Put(key string, val interface{}, expire int64) error {
filename := c.filepath(key)
item := &Item{val, time.Now().Unix(), expire}
data, err := EncodeGob(item)
if err != nil {
return err
}
os.MkdirAll(filepath.Dir(filename), os.ModePerm)
return ioutil.WriteFile(filename, data, os.ModePerm)
}
func (c *FileCacher) read(key string) (*Item, error) {
filename := c.filepath(key)
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
item := new(Item)
return item, DecodeGob(data, item)
}
// Get gets cached value by given key.
func (c *FileCacher) Get(key string) interface{} {
item, err := c.read(key)
if err != nil {
return nil
}
if item.hasExpired() {
os.Remove(c.filepath(key))
return nil
}
return item.Val
}
// Delete deletes cached value by given key.
func (c *FileCacher) Delete(key string) error {
return os.Remove(c.filepath(key))
}
// Incr increases cached int-type value by given key as a counter.
func (c *FileCacher) Incr(key string) error {
item, err := c.read(key)
if err != nil {
return err
}
item.Val, err = Incr(item.Val)
if err != nil {
return err
}
return c.Put(key, item.Val, item.Expire)
}
// Decrease cached int value.
func (c *FileCacher) Decr(key string) error {
item, err := c.read(key)
if err != nil {
return err
}
item.Val, err = Decr(item.Val)
if err != nil {
return err
}
return c.Put(key, item.Val, item.Expire)
}
// IsExist returns true if cached value exists.
func (c *FileCacher) IsExist(key string) bool {
return com.IsExist(c.filepath(key))
}
// Flush deletes all cached data.
func (c *FileCacher) Flush() error {
return os.RemoveAll(c.rootPath)
}
func (c *FileCacher) startGC() {
c.lock.Lock()
defer c.lock.Unlock()
if c.interval < 1 {
return
}
if err := filepath.Walk(c.rootPath, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("Walk: %v", err)
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
fmt.Errorf("ReadFile: %v", err)
}
item := new(Item)
if err = DecodeGob(data, item); err != nil {
return err
}
if item.hasExpired() {
if err = os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("Remove: %v", err)
}
}
return nil
}); err != nil {
log.Printf("error garbage collecting cache files: %v", err)
}
time.AfterFunc(time.Duration(c.interval)*time.Second, func() { c.startGC() })
}
// StartAndGC starts GC routine based on config string settings.
func (c *FileCacher) StartAndGC(opt Options) error {
c.lock.Lock()
c.rootPath = opt.AdapterConfig
c.interval = opt.Interval
if !filepath.IsAbs(c.rootPath) {
c.rootPath = filepath.Join(macaron.Root, c.rootPath)
}
c.lock.Unlock()
if err := os.MkdirAll(c.rootPath, os.ModePerm); err != nil {
return err
}
go c.startGC()
return nil
}
func init() {
Register("file", NewFileCacher())
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"errors"
"sync"
"time"
)
// MemoryItem represents a memory cache item.
type MemoryItem struct {
val interface{}
created int64
expire int64
}
func (item *MemoryItem) hasExpired() bool {
return item.expire > 0 &&
(time.Now().Unix()-item.created) >= item.expire
}
// MemoryCacher represents a memory cache adapter implementation.
type MemoryCacher struct {
lock sync.RWMutex
items map[string]*MemoryItem
interval int // GC interval.
}
// NewMemoryCacher creates and returns a new memory cacher.
func NewMemoryCacher() *MemoryCacher {
return &MemoryCacher{items: make(map[string]*MemoryItem)}
}
// Put puts value into cache with key and expire time.
// If expired is 0, it will be deleted by next GC operation.
func (c *MemoryCacher) Put(key string, val interface{}, expire int64) error {
c.lock.Lock()
defer c.lock.Unlock()
c.items[key] = &MemoryItem{
val: val,
created: time.Now().Unix(),
expire: expire,
}
return nil
}
// Get gets cached value by given key.
func (c *MemoryCacher) Get(key string) interface{} {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
if !ok {
return nil
}
if item.hasExpired() {
go c.Delete(key)
return nil
}
return item.val
}
// Delete deletes cached value by given key.
func (c *MemoryCacher) Delete(key string) error {
c.lock.Lock()
defer c.lock.Unlock()
delete(c.items, key)
return nil
}
// Incr increases cached int-type value by given key as a counter.
func (c *MemoryCacher) Incr(key string) (err error) {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
if !ok {
return errors.New("key not exist")
}
item.val, err = Incr(item.val)
return err
}
// Decr decreases cached int-type value by given key as a counter.
func (c *MemoryCacher) Decr(key string) (err error) {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
if !ok {
return errors.New("key not exist")
}
item.val, err = Decr(item.val)
return err
}
// IsExist returns true if cached value exists.
func (c *MemoryCacher) IsExist(key string) bool {
c.lock.RLock()
defer c.lock.RUnlock()
_, ok := c.items[key]
return ok
}
// Flush deletes all cached data.
func (c *MemoryCacher) Flush() error {
c.lock.Lock()
defer c.lock.Unlock()
c.items = make(map[string]*MemoryItem)
return nil
}
func (c *MemoryCacher) checkRawExpiration(key string) {
item, ok := c.items[key]
if !ok {
return
}
if item.hasExpired() {
delete(c.items, key)
}
}
func (c *MemoryCacher) checkExpiration(key string) {
c.lock.Lock()
defer c.lock.Unlock()
c.checkRawExpiration(key)
}
func (c *MemoryCacher) startGC() {
c.lock.Lock()
defer c.lock.Unlock()
if c.interval < 1 {
return
}
if c.items != nil {
for key, _ := range c.items {
c.checkRawExpiration(key)
}
}
time.AfterFunc(time.Duration(c.interval)*time.Second, func() { c.startGC() })
}
// StartAndGC starts GC routine based on config string settings.
func (c *MemoryCacher) StartAndGC(opt Options) error {
c.lock.Lock()
c.interval = opt.Interval
c.lock.Unlock()
go c.startGC()
return nil
}
func init() {
Register("memory", NewMemoryCacher())
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"bytes"
"encoding/gob"
"errors"
)
func EncodeGob(item *Item) ([]byte, error) {
buf := bytes.NewBuffer(nil)
err := gob.NewEncoder(buf).Encode(item)
return buf.Bytes(), err
}
func DecodeGob(data []byte, out *Item) error {
buf := bytes.NewBuffer(data)
return gob.NewDecoder(buf).Decode(&out)
}
func Incr(val interface{}) (interface{}, error) {
switch val.(type) {
case int:
val = val.(int) + 1
case int32:
val = val.(int32) + 1
case int64:
val = val.(int64) + 1
case uint:
val = val.(uint) + 1
case uint32:
val = val.(uint32) + 1
case uint64:
val = val.(uint64) + 1
default:
return val, errors.New("item value is not int-type")
}
return val, nil
}
func Decr(val interface{}) (interface{}, error) {
switch val.(type) {
case int:
val = val.(int) - 1
case int32:
val = val.(int32) - 1
case int64:
val = val.(int64) - 1
case uint:
if val.(uint) > 0 {
val = val.(uint) - 1
} else {
return val, errors.New("item value is less than 0")
}
case uint32:
if val.(uint32) > 0 {
val = val.(uint32) - 1
} else {
return val, errors.New("item value is less than 0")
}
case uint64:
if val.(uint64) > 0 {
val = val.(uint64) - 1
} else {
return val, errors.New("item value is less than 0")
}
default:
return val, errors.New("item value is not int-type")
}
return val, nil
}
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+16
View File
@@ -0,0 +1,16 @@
# captcha [![Build Status](https://travis-ci.org/go-macaron/captcha.svg?branch=master)](https://travis-ci.org/go-macaron/captcha)
Middleware captcha provides captcha service for [Macaron](https://github.com/go-macaron/macaron).
### Installation
go get github.com/go-macaron/captcha
## Getting Help
- [API Reference](https://gowalker.org/github.com/go-macaron/captcha)
- [Documentation](http://go-macaron.com/docs/middlewares/captcha)
## License
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
+246
View File
@@ -0,0 +1,246 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package captcha a middleware that provides captcha service for Macaron.
package captcha
import (
"fmt"
"html/template"
"path"
"strings"
"github.com/Unknwon/com"
"github.com/go-macaron/cache"
"gopkg.in/macaron.v1"
)
const _VERSION = "0.1.0"
func Version() string {
return _VERSION
}
var (
defaultChars = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
)
// Captcha represents a captcha service.
type Captcha struct {
store cache.Cache
SubURL string
URLPrefix string
FieldIdName string
FieldCaptchaName string
StdWidth int
StdHeight int
ChallengeNums int
Expiration int64
CachePrefix string
}
// generate key string
func (c *Captcha) key(id string) string {
return c.CachePrefix + id
}
// generate rand chars with default chars
func (c *Captcha) genRandChars() string {
return string(com.RandomCreateBytes(c.ChallengeNums, defaultChars...))
}
// CreateHTML outputs HTML for display and fetch new captcha images.
func (c *Captcha) CreateHTML() template.HTML {
value, err := c.CreateCaptcha()
if err != nil {
panic(fmt.Errorf("fail to create captcha: %v", err))
}
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%[1]s" value="%[2]s">
<a class="captcha" href="javascript:" tabindex="-1">
<img onclick="this.src=('%[3]s%[4]s%[2]s.png?reload='+(new Date()).getTime())" class="captcha-img" src="%[3]s%[4]s%[2]s.png">
</a>`, c.FieldIdName, value, c.SubURL, c.URLPrefix))
}
// DEPRECATED
func (c *Captcha) CreateHtml() template.HTML {
return c.CreateHTML()
}
// create a new captcha id
func (c *Captcha) CreateCaptcha() (string, error) {
id := string(com.RandomCreateBytes(15))
if err := c.store.Put(c.key(id), c.genRandChars(), c.Expiration); err != nil {
return "", err
}
return id, nil
}
// verify from a request
func (c *Captcha) VerifyReq(req macaron.Request) bool {
req.ParseForm()
return c.Verify(req.Form.Get(c.FieldIdName), req.Form.Get(c.FieldCaptchaName))
}
// direct verify id and challenge string
func (c *Captcha) Verify(id string, challenge string) bool {
if len(challenge) == 0 || len(id) == 0 {
return false
}
var chars string
key := c.key(id)
if v, ok := c.store.Get(key).(string); ok {
chars = v
} else {
return false
}
defer c.store.Delete(key)
if len(chars) != len(challenge) {
return false
}
// verify challenge
for i, c := range []byte(chars) {
if c != challenge[i]-48 {
return false
}
}
return true
}
type Options struct {
// Suburl path. Default is empty.
SubURL string
// URL prefix of getting captcha pictures. Default is "/captcha/".
URLPrefix string
// Hidden input element ID. Default is "captcha_id".
FieldIdName string
// User input value element name in request form. Default is "captcha".
FieldCaptchaName string
// Challenge number. Default is 6.
ChallengeNums int
// Captcha image width. Default is 240.
Width int
// Captcha image height. Default is 80.
Height int
// Captcha expiration time in seconds. Default is 600.
Expiration int64
// Cache key prefix captcha characters. Default is "captcha_".
CachePrefix string
}
func prepareOptions(options []Options) Options {
var opt Options
if len(options) > 0 {
opt = options[0]
}
opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
// Defaults.
if len(opt.URLPrefix) == 0 {
opt.URLPrefix = "/captcha/"
} else if opt.URLPrefix[len(opt.URLPrefix)-1] != '/' {
opt.URLPrefix += "/"
}
if len(opt.FieldIdName) == 0 {
opt.FieldIdName = "captcha_id"
}
if len(opt.FieldCaptchaName) == 0 {
opt.FieldCaptchaName = "captcha"
}
if opt.ChallengeNums == 0 {
opt.ChallengeNums = 6
}
if opt.Width == 0 {
opt.Width = stdWidth
}
if opt.Height == 0 {
opt.Height = stdHeight
}
if opt.Expiration == 0 {
opt.Expiration = 600
}
if len(opt.CachePrefix) == 0 {
opt.CachePrefix = "captcha_"
}
return opt
}
// NewCaptcha initializes and returns a captcha with given options.
func NewCaptcha(opt Options) *Captcha {
return &Captcha{
SubURL: opt.SubURL,
URLPrefix: opt.URLPrefix,
FieldIdName: opt.FieldIdName,
FieldCaptchaName: opt.FieldCaptchaName,
StdWidth: opt.Width,
StdHeight: opt.Height,
ChallengeNums: opt.ChallengeNums,
Expiration: opt.Expiration,
CachePrefix: opt.CachePrefix,
}
}
// Captchaer is a middleware that maps a captcha.Captcha service into the Macaron handler chain.
// An single variadic captcha.Options struct can be optionally provided to configure.
// This should be register after cache.Cacher.
func Captchaer(options ...Options) macaron.Handler {
return func(ctx *macaron.Context, cache cache.Cache) {
cpt := NewCaptcha(prepareOptions(options))
cpt.store = cache
if strings.HasPrefix(ctx.Req.URL.Path, cpt.URLPrefix) {
var chars string
id := path.Base(ctx.Req.URL.Path)
if i := strings.Index(id, "."); i > -1 {
id = id[:i]
}
key := cpt.key(id)
// Reload captcha.
if len(ctx.Query("reload")) > 0 {
chars = cpt.genRandChars()
if err := cpt.store.Put(key, chars, cpt.Expiration); err != nil {
ctx.Status(500)
ctx.Write([]byte("captcha reload error"))
panic(fmt.Errorf("reload captcha: %v", err))
}
} else {
if v, ok := cpt.store.Get(key).(string); ok {
chars = v
} else {
ctx.Status(404)
ctx.Write([]byte("captcha not found"))
return
}
}
if _, err := NewImage([]byte(chars), cpt.StdWidth, cpt.StdHeight).WriteTo(ctx.Resp); err != nil {
panic(fmt.Errorf("write captcha: %v", err))
}
return
}
ctx.Data["Captcha"] = cpt
ctx.Map(cpt)
}
}
+498
View File
@@ -0,0 +1,498 @@
// Copyright 2013 Beego Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package captcha
import (
"bytes"
"image"
"image/color"
"image/png"
"io"
"math"
)
const (
fontWidth = 11
fontHeight = 18
blackChar = 1
// Standard width and height of a captcha image.
stdWidth = 240
stdHeight = 80
// Maximum absolute skew factor of a single digit.
maxSkew = 0.7
// Number of background circles.
circleCount = 20
)
var font = [][]byte{
{ // 0
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 1
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0,
0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
{ // 2
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
{ // 3
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 4
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0,
1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
},
{ // 5
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 6
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0,
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 7
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
},
{ // 8
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 9
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
},
}
type Image struct {
*image.Paletted
numWidth int
numHeight int
dotSize int
}
var prng = &siprng{}
// randIntn returns a pseudorandom non-negative int in range [0, n).
func randIntn(n int) int {
return prng.Intn(n)
}
// randInt returns a pseudorandom int in range [from, to].
func randInt(from, to int) int {
return prng.Intn(to+1-from) + from
}
// randFloat returns a pseudorandom float64 in range [from, to].
func randFloat(from, to float64) float64 {
return (to-from)*prng.Float64() + from
}
func randomPalette() color.Palette {
p := make([]color.Color, circleCount+1)
// Transparent color.
p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
// Primary color.
prim := color.RGBA{
uint8(randIntn(129)),
uint8(randIntn(129)),
uint8(randIntn(129)),
0xFF,
}
p[1] = prim
// Circle colors.
for i := 2; i <= circleCount; i++ {
p[i] = randomBrightness(prim, 255)
}
return p
}
// NewImage returns a new captcha image of the given width and height with the
// given digits, where each digit must be in range 0-9.
func NewImage(digits []byte, width, height int) *Image {
m := new(Image)
m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), randomPalette())
m.calculateSizes(width, height, len(digits))
// Randomly position captcha inside the image.
maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
maxy := height - m.numHeight - m.dotSize*2
var border int
if width > height {
border = height / 5
} else {
border = width / 5
}
x := randInt(border, maxx-border)
y := randInt(border, maxy-border)
// Draw digits.
for _, n := range digits {
m.drawDigit(font[n], x, y)
x += m.numWidth + m.dotSize
}
// Draw strike-through line.
m.strikeThrough()
// Apply wave distortion.
m.distort(randFloat(5, 10), randFloat(100, 200))
// Fill image with random circles.
m.fillWithCircles(circleCount, m.dotSize)
return m
}
// encodedPNG encodes an image to PNG and returns
// the result as a byte slice.
func (m *Image) encodedPNG() []byte {
var buf bytes.Buffer
if err := png.Encode(&buf, m.Paletted); err != nil {
panic(err.Error())
}
return buf.Bytes()
}
// WriteTo writes captcha image in PNG format into the given writer.
func (m *Image) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(m.encodedPNG())
return int64(n), err
}
func (m *Image) calculateSizes(width, height, ncount int) {
// Goal: fit all digits inside the image.
var border int
if width > height {
border = height / 4
} else {
border = width / 4
}
// Convert everything to floats for calculations.
w := float64(width - border*2)
h := float64(height - border*2)
// fw takes into account 1-dot spacing between digits.
fw := float64(fontWidth + 1)
fh := float64(fontHeight)
nc := float64(ncount)
// Calculate the width of a single digit taking into account only the
// width of the image.
nw := w / nc
// Calculate the height of a digit from this width.
nh := nw * fh / fw
// Digit too high?
if nh > h {
// Fit digits based on height.
nh = h
nw = fw / fh * nh
}
// Calculate dot size.
m.dotSize = int(nh / fh)
// Save everything, making the actual width smaller by 1 dot to account
// for spacing between digits.
m.numWidth = int(nw) - m.dotSize
m.numHeight = int(nh)
}
func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
for x := fromX; x <= toX; x++ {
m.SetColorIndex(x, y, colorIdx)
}
}
func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
f := 1 - radius
dfx := 1
dfy := -2 * radius
xo := 0
yo := radius
m.SetColorIndex(x, y+radius, colorIdx)
m.SetColorIndex(x, y-radius, colorIdx)
m.drawHorizLine(x-radius, x+radius, y, colorIdx)
for xo < yo {
if f >= 0 {
yo--
dfy += 2
f += dfy
}
xo++
dfx += 2
f += dfx
m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
}
}
func (m *Image) fillWithCircles(n, maxradius int) {
maxx := m.Bounds().Max.X
maxy := m.Bounds().Max.Y
for i := 0; i < n; i++ {
colorIdx := uint8(randInt(1, circleCount-1))
r := randInt(1, maxradius)
m.drawCircle(randInt(r, maxx-r), randInt(r, maxy-r), r, colorIdx)
}
}
func (m *Image) strikeThrough() {
maxx := m.Bounds().Max.X
maxy := m.Bounds().Max.Y
y := randInt(maxy/3, maxy-maxy/3)
amplitude := randFloat(5, 20)
period := randFloat(80, 180)
dx := 2.0 * math.Pi / period
for x := 0; x < maxx; x++ {
xo := amplitude * math.Cos(float64(y)*dx)
yo := amplitude * math.Sin(float64(x)*dx)
for yn := 0; yn < m.dotSize; yn++ {
r := randInt(0, m.dotSize)
m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
}
}
}
func (m *Image) drawDigit(digit []byte, x, y int) {
skf := randFloat(-maxSkew, maxSkew)
xs := float64(x)
r := m.dotSize / 2
y += randInt(-r, r)
for yo := 0; yo < fontHeight; yo++ {
for xo := 0; xo < fontWidth; xo++ {
if digit[yo*fontWidth+xo] != blackChar {
continue
}
m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
}
xs += skf
x = int(xs)
}
}
func (m *Image) distort(amplude float64, period float64) {
w := m.Bounds().Max.X
h := m.Bounds().Max.Y
oldm := m.Paletted
newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
dx := 2.0 * math.Pi / period
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
xo := amplude * math.Sin(float64(y)*dx)
yo := amplude * math.Cos(float64(x)*dx)
newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
}
}
m.Paletted = newm
}
func randomBrightness(c color.RGBA, max uint8) color.RGBA {
minc := min3(c.R, c.G, c.B)
maxc := max3(c.R, c.G, c.B)
if maxc > max {
return c
}
n := randIntn(int(max-maxc)) - int(minc)
return color.RGBA{
uint8(int(c.R) + n),
uint8(int(c.G) + n),
uint8(int(c.B) + n),
uint8(c.A),
}
}
func min3(x, y, z uint8) (m uint8) {
m = x
if y < m {
m = y
}
if z < m {
m = z
}
return
}
func max3(x, y, z uint8) (m uint8) {
m = x
if y > m {
m = y
}
if z > m {
m = z
}
return
}
+277
View File
@@ -0,0 +1,277 @@
// Copyright 2013 Beego Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package captcha
import (
"crypto/rand"
"encoding/binary"
"io"
"sync"
)
// siprng is PRNG based on SipHash-2-4.
type siprng struct {
mu sync.Mutex
k0, k1, ctr uint64
}
// siphash implements SipHash-2-4, accepting a uint64 as a message.
func siphash(k0, k1, m uint64) uint64 {
// Initialization.
v0 := k0 ^ 0x736f6d6570736575
v1 := k1 ^ 0x646f72616e646f6d
v2 := k0 ^ 0x6c7967656e657261
v3 := k1 ^ 0x7465646279746573
t := uint64(8) << 56
// Compression.
v3 ^= m
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
v0 ^= m
// Compress last block.
v3 ^= t
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
v0 ^= t
// Finalization.
v2 ^= 0xff
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 3.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 4.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
return v0 ^ v1 ^ v2 ^ v3
}
// rekey sets a new PRNG key, which is read from crypto/rand.
func (p *siprng) rekey() {
var k [16]byte
if _, err := io.ReadFull(rand.Reader, k[:]); err != nil {
panic(err.Error())
}
p.k0 = binary.LittleEndian.Uint64(k[0:8])
p.k1 = binary.LittleEndian.Uint64(k[8:16])
p.ctr = 1
}
// Uint64 returns a new pseudorandom uint64.
// It rekeys PRNG on the first call and every 64 MB of generated data.
func (p *siprng) Uint64() uint64 {
p.mu.Lock()
if p.ctr == 0 || p.ctr > 8*1024*1024 {
p.rekey()
}
v := siphash(p.k0, p.k1, p.ctr)
p.ctr++
p.mu.Unlock()
return v
}
func (p *siprng) Int63() int64 {
return int64(p.Uint64() & 0x7fffffffffffffff)
}
func (p *siprng) Uint32() uint32 {
return uint32(p.Uint64())
}
func (p *siprng) Int31() int32 {
return int32(p.Uint32() & 0x7fffffff)
}
func (p *siprng) Intn(n int) int {
if n <= 0 {
panic("invalid argument to Intn")
}
if n <= 1<<31-1 {
return int(p.Int31n(int32(n)))
}
return int(p.Int63n(int64(n)))
}
func (p *siprng) Int63n(n int64) int64 {
if n <= 0 {
panic("invalid argument to Int63n")
}
max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
v := p.Int63()
for v > max {
v = p.Int63()
}
return v % n
}
func (p *siprng) Int31n(n int32) int32 {
if n <= 0 {
panic("invalid argument to Int31n")
}
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
v := p.Int31()
for v > max {
v = p.Int31()
}
return v % n
}
func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }
-21
View File
@@ -1,21 +0,0 @@
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
View File
@@ -1,40 +0,0 @@
# timewheel
Golang实现的时间轮
![时间轮](https://raw.githubusercontent.com/ouqiang/timewheel/master/timewheel.jpg)
# 安装
```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()
}
```
-126
View File
@@ -1,126 +0,0 @@
package timewheel
import (
"time"
"container/list"
)
// @author qiang.ou<qingqianludao@gmail.com>
type Job func([]interface{})
type TimeWheel struct {
interval time.Duration
ticker *time.Ticker
slots []*list.List
currentPos int
slotNum int
job Job
taskChannel chan Task
stopChannel chan bool
}
type Task struct {
delay time.Duration
circle int
data []interface{}
}
func New(interval time.Duration, slotNum int, job Job) *TimeWheel {
if interval <= 0 || slotNum <= 0 || job == nil {
return nil
}
tw := &TimeWheel{
interval: interval,
slots: make([]*list.List, slotNum),
currentPos: 0,
job: job,
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, data []interface{}) {
if delay <= 0 {
return
}
tw.taskChannel <- Task{delay:delay, data: data}
}
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 tw.job(task.data)
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
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+12 -6
View File
@@ -68,6 +68,18 @@
"revision": "48920167fa152d02f228cfbece7e0f1e452d200a",
"revisionTime": "2016-12-22T07:05:54Z"
},
{
"checksumSHA1": "2P7Mi5cgjw7B0sgNodHpt1MTtwk=",
"path": "github.com/go-macaron/cache",
"revision": "56173531277692bc2925924d51fda1cd0a6b8178",
"revisionTime": "2015-10-13T08:11:02Z"
},
{
"checksumSHA1": "Z14F1jXWg0vXQVCNxqVJEu+VQmg=",
"path": "github.com/go-macaron/captcha",
"revision": "cbfb9d984efb41f44f63e9abaa366a3308ff78ca",
"revisionTime": "2017-03-30T19:07:02Z"
},
{
"checksumSHA1": "60FC18/huiHD1fZ9I3BzK61Pk2Q=",
"path": "github.com/go-macaron/csrf",
@@ -142,12 +154,6 @@
"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": "cVGA2CJTJsCAVa5VKTM8k/ma/BU=",
"path": "github.com/silenceper/pool",