mirror of
https://github.com/ouqiang/gocron.git
synced 2024-04-21 12:31:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ec80a02b6 | ||
|
|
601f250882 | ||
|
|
019fee2cce | ||
|
|
d4e0898674 | ||
|
|
d642c9641d | ||
|
|
2ba8cb67c8 | ||
|
|
350dc0881e | ||
|
|
db1ef3b317 | ||
|
|
1c2696798c | ||
|
|
2b1c7f16cf | ||
|
|
337ee35357 | ||
|
|
b509fcec55 | ||
|
|
6e8622e4f3 | ||
|
|
ff2228ed50 | ||
|
|
5d97ffad3b | ||
|
|
d13f1997dd | ||
|
|
5b8a8ae890 | ||
|
|
3d61c7694b | ||
|
|
3e8c3a236e | ||
|
|
e6560e8483 | ||
|
|
7e010d0aca | ||
|
|
b8f13b4b0e | ||
|
|
eb02804aec | ||
|
|
eb7d8d7bf5 | ||
|
|
b5d476b339 | ||
|
|
a78d14693b | ||
|
|
157e899e06 | ||
|
|
90a3136ce5 | ||
|
|
22069aa156 | ||
|
|
3de70760a0 |
+5
-4
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
### 截图
|
||||

|
||||

|
||||

|
||||
|
||||
@@ -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访问日志输出到终端
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
+46
-7
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -20,6 +20,7 @@ const ResponseFailure = 1
|
||||
const NotFound = 2
|
||||
const AuthError = 3
|
||||
const ServerError = 4
|
||||
const CaptchaError = 5
|
||||
|
||||
const SuccessContent = "操作成功"
|
||||
const FailureContent = "操作失败"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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{}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" . }}}
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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" . }}}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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> <a class="ui blue button" href="/host/create" target="_blank">添加主机</a>
|
||||
</div> <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: [
|
||||
|
||||
@@ -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
@@ -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
@@ -0,0 +1,20 @@
|
||||
# cache [](https://travis-ci.org/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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,16 @@
|
||||
# captcha [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,40 +0,0 @@
|
||||
# timewheel
|
||||
Golang实现的时间轮
|
||||
|
||||
|
||||

|
||||
|
||||
# 安装
|
||||
|
||||
```shell
|
||||
go get -u github.com/ouqiang/timewheel
|
||||
```
|
||||
|
||||
# 使用
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/timewheel"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// tick刻度为1秒, 3600个槽
|
||||
tw := timewheel.New(1 * time.Second, 3600)
|
||||
tw.Start()
|
||||
tw.Add(5 * time.Second, func() {
|
||||
// do something
|
||||
})
|
||||
tw.Add(10 * time.Minute, func() {
|
||||
// do something
|
||||
})
|
||||
tw.Add(35 * time.Hour, func() {
|
||||
// do something
|
||||
})
|
||||
// 停止
|
||||
tw.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
-126
@@ -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
|
||||
}
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Vendored
+12
-6
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user