Compare commits

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

BREAKING CHANGE: 不再支持延迟任务
2017-06-21 19:03:22 +08:00
qiang.ouandGitHub eb02804aec 更新文档 2017-06-14 17:55:34 +08:00
qiang.ouandGitHub eb7d8d7bf5 更新文档 2017-06-14 17:52:00 +08:00
qiang.ouandGitHub b5d476b339 更新文档 2017-06-14 17:48:37 +08:00
ouqiang a78d14693b Merge branch 'master' of github.com:ouqiang/gocron 2017-06-08 21:26:14 +08:00
ouqiang 157e899e06 增加主机连接测试 2017-06-08 21:25:42 +08:00
qiang.ouandGitHub 90a3136ce5 更新文档 2017-06-08 19:08:45 +08:00
ouqiang 22069aa156 增加任务依赖 2017-06-08 18:04:55 +08:00
ouqiang 3de70760a0 完善文档 2017-06-02 14:11:05 +08:00
ouqiang e94a7dc28a 数据库连接池闲置连接数和最大连接数,从配置文件中读取 2017-05-31 20:39:29 +08:00
ouqiang dc60aae381 连接远程服务器失败, 重试60次 2017-05-30 09:46:35 +08:00
ouqiang 49e2c15473 数据库表添加索引 2017-05-29 21:32:22 +08:00
ouqiang 4a524742cc 程序退出, 释放连接池 2017-05-29 17:05:21 +08:00
ouqiang 7633cb8556 关键出入口增加panic捕获 2017-05-29 15:30:59 +08:00
ouqiang 2bf6ea0b6a 顶层函数增加panic捕获 2017-05-29 15:09:41 +08:00
ouqiang 8e6b212b1d gRPC客户端不可用连接, 不放回连接池, 直接关闭 2017-05-29 10:19:58 +08:00
ouqiang 5a8c3391f5 增加gRPC连接池 2017-05-28 23:13:22 +08:00
ouqiang bbed4f4c20 后台开放延时任务日志入口 2017-05-27 22:13:40 +08:00
ouqiang ff0dc6b704 延时任务执行失败,可配置通知URL 2017-05-27 22:04:13 +08:00
ouqiang 499bf561d9 编辑任务后, 自动重新加载 2017-05-27 21:42:18 +08:00
ouqiang 7e68ed9b60 完善文档 2017-05-27 13:33:20 +08:00
ouqiang 527780105f RPC调用超时处理 2017-05-27 13:10:57 +08:00
ouqiang ef996f8536 完善文档 2017-05-26 22:07:58 +08:00
ouqiang 3d5c23128d .gitignore忽略二进制文件 2017-05-26 21:59:30 +08:00
ouqiang 6e0afb91c0 RPC调用使用context控制超时 2017-05-26 21:59:01 +08:00
ouqiang ccad05e50f 新增RPC任务执行, 删除SSH任务 2017-05-26 18:09:07 +08:00
ouqiang 604384a5f4 增加gRPC包 2017-05-25 22:32:40 +08:00
ouqiang df0db4b843 延时任务初始化, 取出待执行、执行中的任务 2017-05-23 15:42:22 +08:00
ouqiang ed521f519b API签名验证是否启用key,由app.sign.enable改为api.sign.enable 2017-05-23 15:30:21 +08:00
ouqiang 2f5fff47d8 API接口增加签名验证 2017-05-23 15:16:39 +08:00
ouqiang 94197e0511 增加gocron命令行管理工具 2017-05-20 16:26:57 +08:00
ouqiang 519c6613c7 生成windows二进制文件, 隐藏窗口, 后台运行 2017-05-20 14:45:28 +08:00
ouqiang 4a77958556 增加以守护进程模式运行 2017-05-20 14:37:40 +08:00
ouqiang 95278d373e 生产环境访问日志写入文件中 2017-05-20 14:13:16 +08:00
ouqiang d91ae4fa44 更新截图 2017-05-18 15:26:17 +08:00
ouqiang 5e8f2da70e 任务列表改为table布局显示 2017-05-18 12:00:55 +08:00
ouqiang 8779b3ad8b 生成二进制文件,去掉调试信息
bugfix#生成压缩包脚本, 当编译失败时, 未及时退出,而是继续往下执行.
2017-05-17 13:34:44 +08:00
ouqiang 28eda835c0 时间轮所有任务共用一个回调函数 2017-05-17 11:49:54 +08:00
235 changed files with 65158 additions and 3793 deletions
+8 -6
View File
@@ -25,12 +25,14 @@ _testmain.go
.idea
log/*
data/sessions/*
data/ssh/password/*
data/ssh/private_key/*
conf/install.lock
conf/app.ini
conf/ansible_hosts.ini
data/*
conf/*
profile/*
public/resource/javascript/vue.js
gocron
gocron.exe
gocron-node
gocron-node.exe
node_modules
package.json
+5 -1
View File
@@ -1,4 +1,8 @@
language: go
go:
- 1.7.x
script: go test `go list ./... | grep -v vendor`
script: go test `go list ./... | grep -v vendor`
notifications:
on_success: never
on_failure: always
+93 -33
View File
@@ -1,23 +1,31 @@
# gocron - 定时任务管理系统
[![Build Status](https://travis-ci.org/ouqiang/gocron.png)](https://travis-ci.org/ouqiang/gocron)
# gocron - 定时任务web管理系统
[![Downloads](https://img.shields.io/github/downloads/ouqiang/gocron/total.svg)](https://github.com/ouqiang/gocron/releases)
[![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/ouqiang/gocron/blob/master/LICENSE)
[![Release](https://img.shields.io/github/release/ouqiang/gocron.svg?label=Release)](https://github.com/ouqiang/gocron/releases)
# 项目简介
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
原有的延时任务拆分为独立项目[延迟队列](https://github.com/ouqiang/delay-queue)
## 功能特性
* 支持任务CURD
* crontab时间表达式精确到秒
* 任务执行失败重试设置
* 任务超时设置
* 延时任务
* 任务执行方式
* 调用本机系统命令
* 通过SSH执行远程命令
* 执行HTTP-GET请求
* 查看任务执行日志
* Web界面管理定时任务
* crontab时间表达式, 精确到秒
* 任务执行失败重试
* 任务执行超时, 强制结束
* 任务依赖配置, A任务完成后再执行B任务
* 账户权限控制
* 任务类型
* shell任务
> 在任务节点上执行shell命令, 支持任务同时在多个节点上运行
* HTTP任务
> 访问指定的URL地址, 由调度器直接执行, 不依赖任务节点
* 查看任务执行结果日志
* 任务执行结果通知, 支持邮件、Slack
### 截图
![流程图](https://raw.githubusercontent.com/ouqiang/gocron/master/scheduler.png)
![任务](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_task.png)
![Slack](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_slack.png)
@@ -29,42 +37,94 @@
## 下载
* [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)
[releases](https://github.com/ouqiang/gocron/releases)
[版本升级](https://github.com/ouqiang/gocron/wiki/版本升级)
## 安装
### 二进制安装
1. 解压压缩包
1. 解压压缩包  
2. `cd 解压目录`
3. 启动
* Windows: `gocron.exe web`
* Linux、Mac OS: `./gocron web`
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`
3. 编译
* 调度器 `go build -tags gocron -o gocron`
* 任务节点 `go build -tags node -o gocron-node`
4. 启动、访问方式同上
### 启动可选参数
### 命令
* -p 端口, 指定端口, 默认5920
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
* -h 查看帮助
* gocron
* -v 查看版本
## 安全
* 使用`https`访问保证数据传输安全, 可在web服务器如nginx中配置https,通过反向代理,访问内部的gocron
* 网站访问设置IP白名单
* SSH登录设置IP白名单
* gocron web
* --host 默认0.0.0.0
* -p 端口, 指定端口, 默认5920
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
* -h 查看帮助
* gocron-node
* -allow-root *nix平台允许以root用户运行
* -s ip:port 监听地址
* -enable-tls 开启TLS
* -ca-file   CA证书文件  
* -cert-file 证书文件
* -key-file 私钥文件
* -h 查看帮助
* -v 查看版本
## To Do List
- [x] 版本升级
- [x] 批量开启、关闭、删除任务
- [x] 调度器与任务节点通信支持https
- [x] 任务分组
- [x] 多用户
- [x] 权限控制
## 程序使用的组件
* web框架 [Macaron](http://go-macaron.com/)
* 定时任务调度 [cron](https://github.com/robfig/cron)
* Web框架 [Macaron](http://go-macaron.com/)
* 定时任务调度 [Cron](https://github.com/robfig/cron)
* ORM [Xorm](https://github.com/go-xorm/xorm)
* UI框架 [Semantic UI](https://semantic-ui.com/)
* 依赖管理(所有依赖包放入vendor目录) [govendor](https://github.com/kardianos/govendor)
* 依赖管理(所有依赖包放入vendor目录) [Govendor](https://github.com/kardianos/govendor)
* RPC框架 [gRPC](https://github.com/grpc/grpc)
## 贡献
欢迎提交PR
## 反馈
提交[issue](https://github.com/ouqiang/gocron/issues/new)
## ChangeLog
v1.3
--------
* 支持多用户登录
* 增加用户权限控制
v1.2.2
--------
* 用户登录页增加图形验证码
* 支持从旧版本升级
* 任务批量开启、关闭、删除
* 调度器与任务节点支持HTTPS双向认证
* 修复任务列表页总记录数显示错误
v1.1
--------
* 任务可同时在多个节点上运行
* *nix平台默认禁止以root用户运行任务节点
* 子任务命令中增加预定义占位符, 子任务可根据主任务运行结果执行相应操作
* 删除守护进程模块
* Web访问日志输出到终端
+21 -7
View File
@@ -16,6 +16,8 @@ OS=''
ARCH=''
# 应用名称
APP_NAME='gocron'
# 版本号
VERSION=''
# 可执行文件名
EXEC_NAME=''
# 压缩包名称
@@ -23,13 +25,15 @@ COMPRESS_FILE=''
# -p 平台 -a 架构
while getopts "p:a:" OPT;
while getopts "p:a:v:" OPT;
do
case $OPT in
p) OS=$OPTARG
;;
a) ARCH=$OPTARG
;;
v) VERSION=$OPTARG
;;
esac
done
@@ -52,23 +56,33 @@ if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
exit 1
fi
echo '开始编译'
GOOS=$OS GOARCH=$ARCH go build
if [[ ! $? ]];then
if [[ -z $VERSION ]];then
echo '版本号不能为空'
exit 1
fi
echo '开始编译调度器'
if [[ $OS = 'windows' ]];then
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
else
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
fi
if [[ $? != 0 ]];then
exit 1
fi
echo '编译完成'
if [[ $OS = 'windows' ]];then
EXEC_NAME=${APP_NAME}.exe
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.zip
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.zip
else
EXEC_NAME=${APP_NAME}
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.tar.gz
fi
mkdir -p $TEMP_DIR/$APP_NAME
if [[ ! $? ]]; then
if [[ $? != 0 ]]; then
exit 1
fi
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# set -x -u
# 任务节点打包, 生成压缩包 gocron-node.zip或gocron-node.tar.gz
# ./build-node.sh -p windows -a amd64
# 参数含义
# -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64
# 目标平台 windows,linux,darwin
OS=''
# 目标平台架构
ARCH=''
# 应用名称
APP_NAME='gocron-node'
# 版本号
VERSION=''
# 可执行文件名
EXEC_NAME=''
# 压缩包名称
COMPRESS_FILE=''
# -p 平台 -a 架构
while getopts "p:a:v:" OPT;
do
case $OPT in
p) OS=$OPTARG
;;
a) ARCH=$OPTARG
;;
v) VERSION=$OPTARG
;;
esac
done
if [[ -z $OS ]];then
echo "平台不能为空"
exit 1
fi
if [[ $OS != 'windows' && $OS != 'linux' && $OS != 'darwin' ]];then
echo '平台错误,支持的平台 windows linux darmin(osx)'
exit 1
fi
if [[ -z $ARCH ]];then
ARCH='amd64'
fi
if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
echo 'arch错误,仅支持 386 amd64'
exit 1
fi
if [[ -z $VERSION ]];then
echo '版本号不能为空'
exit 1
fi
if [[ $OS = 'windows' ]];then
EXEC_NAME=${APP_NAME}.exe
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.zip
else
EXEC_NAME=${APP_NAME}
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.tar.gz
fi
echo '开始编译任务节点'
if [[ $OS = 'windows' ]];then
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
if [[ $? != 0 ]];then
exit 1
fi
echo '编译完成'
if [[ $OS = 'windows' ]];then
zip -rq $COMPRESS_FILE $EXEC_NAME
else
tar czf $COMPRESS_FILE $EXEC_NAME
fi
rm $EXEC_NAME
echo '打包完成'
echo '生成压缩文件--' $COMPRESS_FILE
+161 -142
View File
@@ -1,173 +1,192 @@
package cmd
import (
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/routers"
"github.com/urfave/cli"
"gopkg.in/macaron.v1"
"os"
"os/signal"
"syscall"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/service"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/setting"
"time"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/routers"
"github.com/ouqiang/gocron/service"
"github.com/urfave/cli"
"gopkg.in/macaron.v1"
"os"
"os/signal"
"syscall"
"time"
)
// web服务器默认端口
const DefaultPort = 5920
var CmdWeb = cli.Command{
Name: "web",
Usage: "run web server",
Action: run,
Flags: []cli.Flag{
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
Usage: "bind port number",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
},
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,
Usage: "bind port",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
},
}
func run(ctx *cli.Context) {
// 设置运行环境
setEnvironment(ctx)
// 初始化应用
app.InitEnv()
// 初始化模块 DB、定时任务等
initModule()
// 捕捉信号,配置热更新等
go catchSignal()
m := macaron.Classic()
func runWeb(ctx *cli.Context) {
// 设置运行环境
setEnvironment(ctx)
// 初始化应用
app.InitEnv(ctx.App.Version)
// 初始化模块 DB、定时任务等
initModule()
// 捕捉信号,配置热更新等
go catchSignal()
m := macaron.Classic()
// 注册路由
routers.Register(m)
// 注册中间件.
routers.RegisterMiddleware(m)
port := parsePort(ctx)
m.Run(port)
// 注册路由
routers.Register(m)
// 注册中间件.
routers.RegisterMiddleware(m)
host := parseHost(ctx)
port := parsePort(ctx)
m.Run(host, port)
}
func initModule() {
if !app.Installed {
return
}
func initModule() {
if !app.Installed {
return
}
config, err := setting.Read(app.AppConfig)
if err != nil {
logger.Fatal("读取应用配置失败", err)
}
app.Setting = config
config, err := setting.Read(app.AppConfig)
if err != nil {
logger.Fatal("读取应用配置失败", err)
}
app.Setting = config
models.Db = models.CreateDb()
// 初始化DB
models.Db = models.CreateDb()
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
// 版本升级
upgradeIfNeed()
// 初始化时任务
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)
// 初始化时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
}
// 解析端口
func parsePort(ctx *cli.Context) int {
var port int = DefaultPort
if ctx.IsSet("port") {
port = ctx.Int("port")
}
if port <= 0 || port >= 65535 {
port = DefaultPort
}
var port int = DefaultPort
if ctx.IsSet("port") {
port = ctx.Int("port")
}
if port <= 0 || port >= 65535 {
port = DefaultPort
}
return port
return port
}
func setEnvironment(ctx *cli.Context) {
var env string = "prod"
if ctx.IsSet("env") {
env = ctx.String("env")
}
func parseHost(ctx *cli.Context) string {
if ctx.IsSet("host") {
return ctx.String("host")
}
switch env {
case "test":
macaron.Env = macaron.TEST
case "dev":
macaron.Env = macaron.DEV
default:
macaron.Env = macaron.PROD
}
return "0.0.0.0"
}
func setEnvironment(ctx *cli.Context) {
var env string = "prod"
if ctx.IsSet("env") {
env = ctx.String("env")
}
switch env {
case "test":
macaron.Env = macaron.TEST
case "dev":
macaron.Env = macaron.DEV
default:
macaron.Env = macaron.PROD
}
}
// 捕捉信号
func catchSignal() {
c := make(chan os.Signal)
// todo 配置热更新, windows 不支持 syscall.SIGUSR1, syscall.SIGUSR2
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
for {
s := <- c
logger.Info("收到信号 -- ", s)
switch s {
case syscall.SIGHUP:
logger.Info("收到终端断开信号, 忽略")
case syscall.SIGINT, syscall.SIGTERM:
shutdown()
}
}
func catchSignal() {
c := make(chan os.Signal)
// todo 配置热更新, windows 不支持 syscall.SIGUSR1, syscall.SIGUSR2
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
for {
s := <-c
logger.Info("收到信号 -- ", s)
switch s {
case syscall.SIGHUP:
logger.Info("收到终端断开信号, 忽略")
case syscall.SIGINT, syscall.SIGTERM:
shutdown()
}
}
}
func shutdown() {
if !app.Installed {
os.Exit(0)
return
}
logger.Info("应用准备退出")
serviceTask := new(service.Task)
// 停止所有任务调度
logger.Info("停止定时任务调度")
serviceTask.StopAll()
delayTaskEnable, _ := app.Setting.Key("delay.task.enable").Bool()
if delayTaskEnable {
logger.Info("停止延时任务调度")
serviceDelayTask := new(service.DelayTask)
serviceDelayTask.Stop()
}
taskNumInRunning := service.TaskNum.Num()
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
if taskNumInRunning > 0 {
logger.Info("等待所有任务执行完成后退出")
}
for {
if taskNumInRunning <= 0 {
break
}
time.Sleep(3 * time.Second)
taskNumInRunning = service.TaskNum.Num()
}
logger.Info("已退出")
os.Exit(0)
}
// 应用退出
func shutdown() {
defer func() {
logger.Info("已退出")
os.Exit(0)
}()
if !app.Installed {
return
}
logger.Info("应用准备退出")
serviceTask := new(service.Task)
// 停止所有任务调度
logger.Info("停止定时任务调度")
serviceTask.StopAll()
taskNumInRunning := service.TaskNum.Num()
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
if taskNumInRunning > 0 {
logger.Info("等待所有任务执行完成后退出")
}
for {
if taskNumInRunning <= 0 {
break
}
time.Sleep(3 * time.Second)
taskNumInRunning = service.TaskNum.Num()
}
// 释放gRPC连接池
grpcpool.Pool.ReleaseAll()
}
// 判断应用是否需要升级, 当存在版本号文件且版本小于app.VersionId时升级
func upgradeIfNeed() {
currentVersionId := app.GetCurrentVersionId()
// 没有版本号文件
if currentVersionId == 0 {
return
}
if currentVersionId >= app.VersionId {
return
}
migration := new(models.Migration)
logger.Infof("版本升级开始, 当前版本号%d", currentVersionId)
migration.Upgrade(currentVersionId)
app.UpdateVersionFile()
logger.Infof("已升级到最新版本%d", app.VersionId)
}
View File
View File
View File
+68
View File
@@ -0,0 +1,68 @@
// +build node
// 任务节点
package main
import (
"flag"
"fmt"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/ouqiang/gocron/modules/rpc/server"
"github.com/ouqiang/gocron/modules/utils"
"os"
"runtime"
"strings"
)
const AppVersion = "1.3"
func main() {
var serverAddr string
var allowRoot bool
var version bool
var CAFile string
var certFile string
var keyFile string
var enableTLS bool
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.BoolVar(&version, "v", false, "./gocron-node -v")
flag.BoolVar(&enableTLS, "enable-tls", false, "./gocron-node -enable-tls")
flag.StringVar(&CAFile, "ca-file", "", "./gocron-node -ca-file path")
flag.StringVar(&certFile, "cert-file", "", "./gocron-node -cert-file path")
flag.StringVar(&keyFile, "key-file", "", "./gocron-node -key-file path")
flag.Parse()
if version {
fmt.Println(AppVersion)
return
}
if enableTLS {
if !utils.FileExist(CAFile) {
fmt.Printf("failed to read ca cert file: %s", CAFile)
return
}
if !utils.FileExist(certFile) {
fmt.Printf("failed to read server cert file: %s", certFile)
return
}
if !utils.FileExist(keyFile) {
fmt.Printf("failed to read server key file: %s", keyFile)
return
}
}
certificate := auth.Certificate{
CAFile: strings.TrimSpace(CAFile),
CertFile: strings.TrimSpace(certFile),
KeyFile: strings.TrimSpace(keyFile),
}
if runtime.GOOS != "windows" && os.Getuid() == 0 && !allowRoot {
fmt.Println("Do not run gocron-node as root user")
return
}
server.Start(serverAddr, enableTLS, certificate)
}
+16 -19
View File
@@ -1,28 +1,25 @@
// +build gocron
// 调度中心
package main
/*--------------------------------------------------------
定时任务调度
兼容Linux crontab时间格式语法,最小粒度可精确到每秒
支持通过HTTP、SSH协议执行任务
--------------------------------------------------------*/
import (
"github.com/urfave/cli"
"os"
"github.com/urfave/cli"
"os"
"github.com/ouqiang/gocron/cmd"
"github.com/ouqiang/gocron/cmd"
)
const AppVersion = "0.2"
const AppVersion = "1.3"
func main() {
app := cli.NewApp()
app.Name = "gocron"
app.Usage = "gocron service"
app.Version = AppVersion
app.Commands = []cli.Command{
cmd.CmdWeb,
}
app.Flags = append(app.Flags, []cli.Flag{}...)
app.Run(os.Args)
app := cli.NewApp()
app.Name = "gocron"
app.Usage = "gocron service"
app.Version = AppVersion
app.Commands = []cli.Command{
cmd.CmdWeb,
}
app.Flags = append(app.Flags, []cli.Flag{}...)
app.Run(os.Args)
}
-85
View File
@@ -1,85 +0,0 @@
package models
import (
"time"
"github.com/go-xorm/xorm"
)
// 延迟任务
type DelayTask struct {
Id int64 `xorm:"bigint pk autoincr"`
Url string `xorm:"varchar(128) not null"`
Params string `xorm:"varchar(256) not null default '' "`
Delay int `xorm:"mediumint notnull default 0"` // 延迟时间
Status Status `xorm:"tinyint notnull index(u_status_created) default 5"` // 状态 0:执行失败 1:执行中 2:执行成功 5: 待执行
Created time.Time `xorm:"datetime notnull created index(u_status_created)"`
Updated time.Time `xorm:"datetime updated"`
BaseModel `xorm:"-"`
}
func (task *DelayTask) Create() (insertId int64, err error) {
_, err = Db.Insert(task)
if err == nil {
insertId = task.Id
}
return
}
// 获取所有待执行任务
func (task *DelayTask) ActiveList(endTime time.Time) ([]DelayTask, error) {
list := make([]DelayTask, 0)
fields := "id,url,params,delay,created"
err := Db.Where("status = ? AND created <= ?", Waiting, endTime.Format(DefaultTimeFormat)).Cols(fields).Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
return list, err
}
// 获取待执行任务数量
func (task *DelayTask) ActiveNum(endTime time.Time) (int, error) {
count ,err := Db.Where("status = ? AND created <= ?", Waiting, endTime.Format(DefaultTimeFormat)).Count(task)
return int(count), err
}
func (task *DelayTask) List(params CommonMap) ([]DelayTask, error) {
task.parsePageAndPageSize(params)
list := make([]DelayTask, 0)
session := Db.Desc("id")
task.parseWhere(session, params)
err := session.Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
return list, err
}
// 更新任务状态
func (task *DelayTask) UpdateStatus(id int64, status Status) (int64, error) {
return Db.Table(task).Id(id).Update(CommonMap{
"status": status,
})
}
// 解析where
func (task *DelayTask) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
status, ok := params["Status"]
if ok && status.(int) > -1 {
session.And("status = ?", status)
}
}
// 删除N个月前的日志
func (task *DelayTask) Remove(id int) (int64, error) {
t := time.Now().AddDate(0, -id, 0)
return Db.Where("created <= ?", t.Format(DefaultTimeFormat)).Delete(task)
}
func (task *DelayTask) Total(params CommonMap) (int64, error) {
session := Db.NewSession()
task.parseWhere(session, params)
return session.Count(task)
}
+51 -92
View File
@@ -1,135 +1,94 @@
package models
import (
"github.com/ouqiang/gocron/modules/ssh"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/utils"
"errors"
"io/ioutil"
"strings"
"github.com/ouqiang/gocron/modules/logger"
"github.com/go-xorm/xorm"
)
// 主机
type Host struct {
Id int16 `xorm:"smallint pk autoincr"`
Name string `xorm:"varchar(64) notnull"` // 主机名称
Alias string `xorm:"varchar(32) notnull default '' "` // 主机别名
Username string `xorm:"varchar(32) notnull default '' "` // ssh 用户名
Port int `xorm:"notnull default 22"` // 主机端口
Remark string `xorm:"varchar(100) notnull default '' "` // 备注
AuthType ssh.HostAuthType `xorm:"tinyint notnull default 1"` // 认证方式 1: 密码 2: 公钥
BaseModel `xorm:"-"`
Id int16 `xorm:"smallint pk autoincr"`
Name string `xorm:"varchar(64) notnull"` // 主机名称
Alias string `xorm:"varchar(32) notnull default '' "` // 主机别名
Port int `xorm:"notnull default 22"` // 主机端口
Remark string `xorm:"varchar(100) notnull default '' "` // 备注
BaseModel `xorm:"-"`
Selected bool `xorm:"-"`
}
// 新增
func (host *Host) Create() (insertId int16, err error) {
_, err = Db.Insert(host)
if err == nil {
insertId = host.Id
}
_, err = Db.Insert(host)
if err == nil {
insertId = host.Id
}
return
return
}
func (host *Host) UpdateBean(id int16) (int64, error) {
return Db.ID(id).Cols("name,alias,username,port,remark,auth_type").Update(host)
func (host *Host) UpdateBean(id int16) (int64, error) {
return Db.ID(id).Cols("name,alias,port,remark").Update(host)
}
// 更新
func (host *Host) Update(id int, data CommonMap) (int64, error) {
return Db.Table(host).ID(id).Update(data)
return Db.Table(host).ID(id).Update(data)
}
// 删除
func (host *Host) Delete(id int) (int64, error) {
return Db.Id(id).Delete(host)
return Db.Id(id).Delete(new(Host))
}
func (host *Host) Find(id int) error {
_, err := Db.Id(id).Get(host)
_, err := Db.Id(id).Get(host)
return err
return err
}
func (host *Host) NameExists(name string, id int16) (bool, error) {
if id == 0 {
count, err := Db.Where("name = ?", name).Count(host);
return count > 0, err
}
func (host *Host) NameExists(name string, id int16) (bool, error) {
if id == 0 {
count, err := Db.Where("name = ?", name).Count(host)
return count > 0, err
}
count, err := Db.Where("name = ? AND id != ?", name, id).Count(host);
return count > 0, err
count, err := Db.Where("name = ? AND id != ?", name, id).Count(host)
return count > 0, err
}
func (host *Host) List(params CommonMap) ([]Host, error) {
host.parsePageAndPageSize(params)
list := make([]Host, 0)
session := Db.Desc("id")
host.parseWhere(session, params)
err := session.Limit(host.PageSize, host.pageLimitOffset()).Find(&list)
host.parsePageAndPageSize(params)
list := make([]Host, 0)
session := Db.Desc("id")
host.parseWhere(session, params)
err := session.Limit(host.PageSize, host.pageLimitOffset()).Find(&list)
return list, err
return list, err
}
func (host *Host) AllList() ([]Host, error) {
list := make([]Host, 0)
err := Db.Desc("id").Find(&list)
list := make([]Host, 0)
err := Db.Cols("name,port").Desc("id").Find(&list)
return list, err
return list, err
}
func (host *Host) Total(params CommonMap) (int64, error) {
session := Db.NewSession()
host.parseWhere(session, params)
return session.Count(host)
}
func (h *Host) GetPasswordByHost(host string) (string, error) {
path := app.DataDir + "/ssh/password/" + host
return h.readFile(path)
}
func (h *Host) GetPrivateKeyByHost(host string) (string,error) {
path := app.DataDir + "/ssh/private_key/" + host
return h.readFile(path)
}
func (host *Host) readFile(file string) (string, error) {
logger.Debug("认证文件路径: ", file)
if !utils.FileExist(file) {
return "", errors.New(file + "-认证文件不存在或无权限访问")
}
contentByte, err := ioutil.ReadFile(file)
if err != nil {
return "", err
}
content := string(contentByte)
content = strings.TrimSpace(content)
if content == "" {
return "", errors.New("密码为空")
}
return content, nil
session := Db.NewSession()
host.parseWhere(session, params)
return session.Count(host)
}
// 解析where
func (host *Host) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
id, ok := params["Id"]
if ok && id.(int) > 0 {
session.And("id = ?", id)
}
name, ok := params["Name"]
if ok && name.(string) != "" {
session.And("name = ?", name)
}
}
func (host *Host) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
id, ok := params["Id"]
if ok && id.(int) > 0 {
session.And("id = ?", id)
}
name, ok := params["Name"]
if ok && name.(string) != "" {
session.And("name = ?", name)
}
}
+17 -17
View File
@@ -1,36 +1,36 @@
package models
import (
"time"
"time"
)
// 用户登录日志
type LoginLog struct {
Id int `xorm:"pk autoincr notnull "`
Username string `xorm:"varchar(32) notnull"`
Ip string `xorm:"varchar(15) not null"`
Created time.Time `xorm:"datetime notnull created"`
BaseModel `xorm:"-"`
Id int `xorm:"pk autoincr notnull "`
Username string `xorm:"varchar(32) notnull"`
Ip string `xorm:"varchar(15) not null"`
Created time.Time `xorm:"datetime notnull created"`
BaseModel `xorm:"-"`
}
func (log *LoginLog) Create() (insertId int, err error) {
_, err = Db.Insert(log)
if err == nil {
insertId = log.Id
}
_, err = Db.Insert(log)
if err == nil {
insertId = log.Id
}
return
return
}
func (log *LoginLog) List(params CommonMap) ([]LoginLog, error) {
log.parsePageAndPageSize(params)
list := make([]LoginLog, 0)
err := Db.Desc("id").Limit(log.PageSize, log.pageLimitOffset()).Find(&list)
log.parsePageAndPageSize(params)
list := make([]LoginLog, 0)
err := Db.Desc("id").Limit(log.PageSize, log.pageLimitOffset()).Find(&list)
return list, err
return list, err
}
func (log *LoginLog) Total() (int64, error) {
return Db.Count(log)
}
return Db.Count(log)
}
+158 -31
View File
@@ -1,44 +1,171 @@
package models
import (
"errors"
"errors"
"fmt"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/logger"
"strconv"
)
// 创建数据库表
type Migration struct{}
func (migration *Migration) Exec(dbName string) error {
if !isDatabaseExist(dbName) {
return errors.New("数据库不存在")
}
setting := new(Setting)
task := new(Task)
tables := []interface{}{
&User{}, task, &DelayTask{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
}
for _, table := range tables {
exist, err:= Db.IsTableExist(table)
if exist {
return errors.New("数据表已存在")
}
if err != nil {
return err
}
err = Db.Sync2(table)
if err != nil {
return err
}
}
setting.InitBasicField()
task.CreateTestTask()
// 首次安装, 创建数据库表
func (migration *Migration) Install(dbName string) error {
if !isDatabaseExist(dbName) {
return errors.New("数据库不存在")
}
setting := new(Setting)
task := new(Task)
tables := []interface{}{
&User{}, task, &TaskLog{}, &Host{}, setting, &LoginLog{}, &TaskHost{},
}
for _, table := range tables {
exist, err := Db.IsTableExist(table)
if exist {
return errors.New("数据表已存在")
}
if err != nil {
return err
}
err = Db.Sync2(table)
if err != nil {
return err
}
}
setting.InitBasicField()
task.CreateTestTask()
return nil
return nil
}
// 创建数据库
// 判断数据库是否存在
func isDatabaseExist(name string) bool {
_, err := Db.Exec("use ?", name)
_, err := Db.Exec("use ?", name)
return err != nil
return err != nil
}
// 迭代升级数据库, 新建表、新增字段等
func (migration *Migration) Upgrade(oldVersionId int) {
// v1.2版本不支持升级
if oldVersionId == 120 {
return
}
versionIds := []int{110, 122, 130}
upgradeFuncs := []func(*xorm.Session) error{
migration.upgradeFor110,
migration.upgradeFor122,
migration.upgradeFor130,
}
startIndex := -1
// 从当前版本的下一版本开始升级
for i, value := range versionIds {
if value > oldVersionId {
startIndex = i
break
}
}
if startIndex == -1 {
return
}
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
}
// 升级到1.2.2版本
func (migration *Migration) upgradeFor122(session *xorm.Session) error {
logger.Info("开始升级到v1.2.2")
tableName := TablePrefix + "task"
// task表增加tag字段
_, err := session.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN tag VARCHAR(32) NOT NULL DEFAULT '' ", tableName))
logger.Info("已升级到v1.2.2\n")
return err
}
// 升级到1.2.3版本
func (migration *Migration) upgradeFor130(session *xorm.Session) error {
logger.Info("开始升级到v1.3")
tableName := TablePrefix + "user"
// 删除user表deleted字段
_, err := session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN deleted", tableName))
logger.Info("已升级到v1.3\n")
return err
}
+81 -96
View File
@@ -1,15 +1,16 @@
package models
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"gopkg.in/macaron.v1"
"strings"
"time"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/app"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"gopkg.in/macaron.v1"
"strings"
"time"
)
type Status int8
@@ -19,120 +20,104 @@ var TablePrefix string = ""
var Db *xorm.Engine
const (
Disabled Status = 0 // 禁用
Failure Status = 0 // 失败
Enabled Status = 1 // 启用
Running Status = 1 // 运行中
Finish Status = 2 // 完成
Cancel Status = 3 // 取消
Background Status = 4 // 后台运行
Waiting Status = 5 // 等待中
Disabled Status = 0 // 禁用
Failure Status = 0 // 失败
Enabled Status = 1 // 启用
Running Status = 1 // 运行中
Finish Status = 2 // 完成
Cancel Status = 3 // 取消
)
const (
Page = 1 // 当前页数
PageSize = 20 // 每页多少条数据
MaxPageSize = 100000 // 每次最多取多少条
Page = 1 // 当前页数
PageSize = 20 // 每页多少条数据
MaxPageSize = 100000 // 每次最多取多少条
)
const DefaultTimeFormat = "2006-01-02 15:04:05"
type BaseModel struct {
Page int `xorm:"-"`
PageSize int `xorm:"-"`
type BaseModel struct {
Page int `xorm:"-"`
PageSize int `xorm:"-"`
}
func (model *BaseModel) parsePageAndPageSize(params CommonMap) {
page, ok := params["Page"]
if ok {
model.Page = page.(int)
}
pageSize, ok := params["PageSize"]
if ok {
model.PageSize = pageSize.(int)
}
if model.Page <= 0 {
model.Page = Page
}
if model.PageSize <= 0 || model.PageSize > MaxPageSize {
model.PageSize = PageSize
}
page, ok := params["Page"]
if ok {
model.Page = page.(int)
}
pageSize, ok := params["PageSize"]
if ok {
model.PageSize = pageSize.(int)
}
if model.Page <= 0 {
model.Page = Page
}
if model.PageSize <= 0 {
model.PageSize = MaxPageSize
}
}
func (model *BaseModel) pageLimitOffset() int {
return (model.Page - 1) * model.PageSize
return (model.Page - 1) * model.PageSize
}
// 创建Db
func CreateDb() *xorm.Engine {
config := getDbConfig()
dsn := getDbEngineDSN(config["engine"], config)
engine, err := xorm.NewEngine(config["engine"], dsn)
if err != nil {
logger.Fatal("创建xorm引擎失败", err)
}
if config["prefix"] != "" {
// 设置表前缀
TablePrefix = config["prefix"]
mapper := core.NewPrefixMapper(core.SnakeMapper{}, config["prefix"])
engine.SetTableMapper(mapper)
}
// 本地环境开启日志
if macaron.Env == macaron.DEV {
engine.ShowSQL(true)
engine.Logger().SetLevel(core.LOG_DEBUG)
}
dsn := getDbEngineDSN(app.Setting)
engine, err := xorm.NewEngine(app.Setting.Db.Engine, dsn)
if err != nil {
logger.Fatal("创建xorm引擎失败", err)
}
engine.SetMaxIdleConns(app.Setting.Db.MaxIdleConns)
engine.SetMaxOpenConns(app.Setting.Db.MaxOpenConns)
go keepDbAlived(engine)
if app.Setting.Db.Prefix != "" {
// 设置表前缀
TablePrefix = app.Setting.Db.Prefix
mapper := core.NewPrefixMapper(core.SnakeMapper{}, app.Setting.Db.Prefix)
engine.SetTableMapper(mapper)
}
// 本地环境开启日志
if macaron.Env == macaron.DEV {
engine.ShowSQL(true)
engine.Logger().SetLevel(core.LOG_DEBUG)
}
return engine
go keepDbAlived(engine)
return engine
}
// 创建临时数据库连接
func CreateTmpDb(config map[string]string) (*xorm.Engine, error) {
dsn := getDbEngineDSN(config["engine"], config)
func CreateTmpDb(setting *setting.Setting) (*xorm.Engine, error) {
dsn := getDbEngineDSN(setting)
return xorm.NewEngine(config["engine"], dsn)
return xorm.NewEngine(setting.Db.Engine, dsn)
}
// 获取数据库引擎DSN mysql,sqlite
func getDbEngineDSN(engine string, config map[string]string) string {
engine = strings.ToLower(engine)
var dsn string = ""
switch engine {
case "mysql":
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s",
config["user"],
config["password"],
config["host"],
config["port"],
config["database"],
config["charset"])
}
func getDbEngineDSN(setting *setting.Setting) string {
engine := strings.ToLower(setting.Db.Engine)
var dsn string = ""
switch engine {
case "mysql":
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s",
setting.Db.User,
setting.Db.Password,
setting.Db.Host,
setting.Db.Port,
setting.Db.Database,
setting.Db.Charset)
}
return dsn
return dsn
}
// 定时ping, 防止因数据库超时设置连接被断开
func keepDbAlived(engine *xorm.Engine) {
t := time.Tick(180 * time.Second)
for {
<- t
engine.Ping()
}
func keepDbAlived(engine *xorm.Engine) {
t := time.Tick(180 * time.Second)
for {
<-t
engine.Ping()
}
}
// 获取数据库配置
func getDbConfig() map[string]string {
var db map[string]string = make(map[string]string)
db["user"] = app.Setting.Key("db.user").String()
db["password"] = app.Setting.Key("db.password").String()
db["host"] = app.Setting.Key("db.host").String()
db["port"] = app.Setting.Key("db.port").String()
db["database"] = app.Setting.Key("db.database").String()
db["charset"] = app.Setting.Key("db.charset").String()
db["prefix"] = app.Setting.Key("db.prefix").String()
db["engine"] = app.Setting.Key("db.engine").String()
return db
}
+100 -100
View File
@@ -1,14 +1,14 @@
package models
import (
"encoding/json"
"encoding/json"
)
type Setting struct {
Id int `xorm:"int pk autoincr"`
Code string `xorm:"varchar(32) notnull"`
Key string `xorm:"varchar(64) notnull"`
Value string `xorm:"varchar(4096) notnull default '' "`
type Setting struct {
Id int `xorm:"int pk autoincr"`
Code string `xorm:"varchar(32) notnull"`
Key string `xorm:"varchar(64) notnull"`
Value string `xorm:"varchar(4096) notnull default '' "`
}
const SlackCode = "slack"
@@ -21,154 +21,154 @@ const MailUserKey = "user"
// 初始化基本字段 邮件、slack等
func (setting *Setting) InitBasicField() {
setting.Code = SlackCode;
setting.Key = SlackUrlKey
Db.Insert(setting)
setting.Code = SlackCode
setting.Key = SlackUrlKey
Db.Insert(setting)
setting.Id = 0
setting.Code = MailCode
setting.Key = MailServerKey
Db.Insert(setting)
setting.Id = 0
setting.Code = MailCode
setting.Key = MailServerKey
Db.Insert(setting)
}
// region slack配置
type Slack struct {
Url string
Channels []Channel
Url string
Channels []Channel
}
type Channel struct {
Id int
Name string
Id int
Name string
}
func (setting *Setting) Slack() (Slack, error) {
list := make([]Setting, 0)
err := Db.Where("code = ?", SlackCode).Find(&list)
slack := Slack{Url:"", Channels:make([]Channel, 0)}
if err != nil {
return slack, err
}
func (setting *Setting) Slack() (Slack, error) {
list := make([]Setting, 0)
err := Db.Where("code = ?", SlackCode).Find(&list)
slack := Slack{Url: "", Channels: make([]Channel, 0)}
if err != nil {
return slack, err
}
setting.formatSlack(list, &slack)
setting.formatSlack(list, &slack)
return slack, err
return slack, err
}
func (setting *Setting) formatSlack(list []Setting, slack *Slack) {
for _, v := range list {
if v.Key == SlackUrlKey {
slack.Url = v.Value
continue
}
func (setting *Setting) formatSlack(list []Setting, slack *Slack) {
for _, v := range list {
if v.Key == SlackUrlKey {
slack.Url = v.Value
continue
}
slack.Channels = append(slack.Channels, Channel{
v.Id, v.Value,
})
}
slack.Channels = append(slack.Channels, Channel{
v.Id, v.Value,
})
}
}
// 更新slack webhook url
func (setting *Setting) UpdateSlackUrl(url string) (int64, error) {
setting.Value = url
setting.Value = url
return Db.Cols("value").Update(setting, Setting{Code:SlackCode, Key:SlackUrlKey})
return Db.Cols("value").Update(setting, Setting{Code: SlackCode, Key: SlackUrlKey})
}
// 创建slack渠道
func (setting *Setting) CreateChannel(channel string) (int64, error) {
setting.Code = SlackCode
setting.Key = SlackChannelKey
setting.Value = channel
setting.Code = SlackCode
setting.Key = SlackChannelKey
setting.Value = channel
return Db.Insert(setting)
return Db.Insert(setting)
}
func (setting *Setting) IsChannelExist(channel string) (bool) {
setting.Code = SlackCode
setting.Key = SlackChannelKey
setting.Value = channel
func (setting *Setting) IsChannelExist(channel string) bool {
setting.Code = SlackCode
setting.Key = SlackChannelKey
setting.Value = channel
count, _ := Db.Count(setting)
count, _ := Db.Count(setting)
return count > 0
return count > 0
}
// 删除slack渠道
func (setting *Setting) RemoveChannel(id int) (int64, error) {
setting.Code = SlackCode
setting.Key = SlackChannelKey
setting.Id = id
return Db.Delete(setting)
func (setting *Setting) RemoveChannel(id int) (int64, error) {
setting.Code = SlackCode
setting.Key = SlackChannelKey
setting.Id = id
return Db.Delete(setting)
}
// endregion
type Mail struct {
Host string
Port int
User string
Password string
MailUsers []MailUser
Host string
Port int
User string
Password string
MailUsers []MailUser
}
type MailUser struct {
Id int
Username string
Email string
Id int
Username string
Email string
}
// region 邮件配置
func (setting *Setting) Mail() (Mail, error) {
list := make([]Setting, 0)
err := Db.Where("code = ?", MailCode).Find(&list)
mail := Mail{MailUsers:make([]MailUser, 0)}
if err != nil {
return mail, err
}
func (setting *Setting) Mail() (Mail, error) {
list := make([]Setting, 0)
err := Db.Where("code = ?", MailCode).Find(&list)
mail := Mail{MailUsers: make([]MailUser, 0)}
if err != nil {
return mail, err
}
setting.formatMail(list, &mail)
setting.formatMail(list, &mail)
return mail, err
return mail, err
}
func (setting *Setting) formatMail(list []Setting, mail *Mail) {
mailUser := MailUser{}
for _, v := range list {
if v.Key == MailServerKey {
json.Unmarshal([]byte(v.Value), mail)
continue
}
json.Unmarshal([]byte(v.Value), &mailUser)
mailUser.Id = v.Id
mail.MailUsers = append(mail.MailUsers, mailUser)
}
func (setting *Setting) formatMail(list []Setting, mail *Mail) {
mailUser := MailUser{}
for _, v := range list {
if v.Key == MailServerKey {
json.Unmarshal([]byte(v.Value), mail)
continue
}
json.Unmarshal([]byte(v.Value), &mailUser)
mailUser.Id = v.Id
mail.MailUsers = append(mail.MailUsers, mailUser)
}
}
func (setting *Setting) UpdateMailServer(config string) (int64, error) {
setting.Value = config
return Db.Cols("value").Update(setting, Setting{Code:MailCode, Key:MailServerKey})
func (setting *Setting) UpdateMailServer(config string) (int64, error) {
setting.Value = config
return Db.Cols("value").Update(setting, Setting{Code: MailCode, Key: MailServerKey})
}
func (setting *Setting) CreateMailUser(username, email string) (int64, error) {
setting.Code = MailCode
setting.Key = MailUserKey
mailUser := MailUser{0, username, email}
jsonByte, err := json.Marshal(mailUser)
if err != nil {
return 0, err
}
setting.Value = string(jsonByte)
setting.Code = MailCode
setting.Key = MailUserKey
mailUser := MailUser{0, username, email}
jsonByte, err := json.Marshal(mailUser)
if err != nil {
return 0, err
}
setting.Value = string(jsonByte)
return Db.Insert(setting)
return Db.Insert(setting)
}
func (setting *Setting) RemoveMailUser(id int) (int64, error) {
setting.Code = MailCode
setting.Key = MailUserKey
setting.Id = id
return Db.Delete(setting)
func (setting *Setting) RemoveMailUser(id int) (int64, error) {
setting.Code = MailCode
setting.Key = MailUserKey
setting.Id = id
return Db.Delete(setting)
}
// endregion
// endregion
+202 -132
View File
@@ -1,201 +1,271 @@
package models
import (
"time"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/utils"
"errors"
"github.com/go-xorm/xorm"
"strings"
"time"
)
type TaskProtocol int8
const (
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
TaskSSH // SSH命令
TaskLocalCommand // 本地命令
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
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"` // 任务名称
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command 3: 系统命令
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"` // SSH 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:停止
Created time.Time `xorm:"datetime notnull created"` // 创建时间
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
BaseModel `xorm:"-"`
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 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"` // 重试次数
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逗号分隔
Tag string `xorm:"varchar(32) notnull default ''"`
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
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
Username string
Alias string
AuthType ssh.HostAuthType
}
func (TaskHost) TableName() string {
return TablePrefix + "task"
func taskHostTableName() []string {
return []string{TablePrefix + "task_host", "th"}
}
// 新增
func (task *Task) Create() (insertId int, err error) {
_, err = Db.Insert(task)
if err == nil {
insertId = task.Id
}
_, err = Db.Insert(task)
if err == nil {
insertId = task.Id
}
return
return
}
// 新增测试任务
func (task *Task) CreateTestTask() {
// HTTP任务
task.Name = "测试HTTP任务"
task.Protocol = TaskHTTP
task.Spec = "*/30 * * * * *"
// 查询IP地址区域信息
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
task.Status = Enabled
task.Create()
// 系统命令
task.Id = 0
task.Name = "测试系统命令任务"
task.Protocol = TaskLocalCommand
task.Spec = "@every 1m"
task.Status = Enabled
if utils.IsWindows() {
task.Command = "dir"
} else {
task.Command = "ls"
}
task.Create()
// HTTP任务
task.Name = "测试HTTP任务"
task.Level = TaskLevelParent
task.Protocol = TaskHTTP
task.Spec = "*/30 * * * * *"
task.Tag = "test-task"
// 查询IP地址区域信息
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
task.Status = Enabled
task.Create()
}
func (task *Task) UpdateBean(id int) (int64, error) {
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,notify_status,notify_type,notify_receiver_id").Update(task)
func (task *Task) UpdateBean(id int) (int64, error) {
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, tag").
Update(task)
}
// 更新
func (task *Task) Update(id int, data CommonMap) (int64, error) {
return Db.Table(task).ID(id).Update(data)
return Db.Table(task).ID(id).Update(data)
}
// 删除
func (task *Task) Delete(id int) (int64, error) {
return Db.Id(id).Delete(task)
return Db.Id(id).Delete(task)
}
// 禁用
func (task *Task) Disable(id int) (int64, error) {
return task.Update(id, CommonMap{"status": Disabled})
return task.Update(id, CommonMap{"status": Disabled})
}
// 激活
func (task *Task) Enable(id int) (int64, error) {
return task.Update(id, CommonMap{"status": Enabled})
return task.Update(id, CommonMap{"status": Enabled})
}
// 获取所有激活任务
func (task *Task) ActiveList() ([]TaskHost, error) {
list := make([]TaskHost, 0)
fields := "t.*, host.alias,host.name,host.username,host.port,host.auth_type"
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.username,host.port,host.auth_type"
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
}
// 判断任务名称是否存在
func (task *Task) NameExist(name string, id int) (bool, error) {
if id > 0 {
count, err := Db.Where("name = ? AND status = ? AND id != ?", name, Enabled, id).Count(task);
return count > 0, err
}
count, err := Db.Where("name = ? AND status = ?", name, Enabled).Count(task);
func (task *Task) NameExist(name string, id int) (bool, error) {
if id > 0 {
count, err := Db.Where("name = ? AND status = ? AND id != ?", name, Enabled, id).Count(task)
return count > 0, err
}
count, err := Db.Where("name = ? AND status = ?", name, Enabled).Count(task)
return count > 0, err
return count > 0, err
}
func(task *Task) Detail(id int) (TaskHost, error) {
taskHost := TaskHost{}
fields := "t.*, host.alias,host.name,host.username,host.port,host.auth_type"
_, err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.id=?", id).Cols(fields).Get(&taskHost)
func (task *Task) GetStatus(id int) (Status, error) {
exist, err := Db.Id(id).Get(task)
if err != nil {
return 0, err
}
if !exist {
return 0, errors.New("not exist")
}
return taskHost, err
return task.Status, nil
}
func (task *Task) List(params CommonMap) ([]TaskHost, 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")
task.parseWhere(session, params)
err := session.Cols(fields).Desc("t.id").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
func (task *Task) Detail(id int) (Task, error) {
t := Task{}
_, err := Db.Where("id=?", id).Get(&t)
return list, 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) ([]Task, error) {
task.parsePageAndPageSize(params)
list := make([]Task, 0)
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
task.parseWhere(session, params)
err := session.GroupBy("t.id").Desc("t.id").Cols("t.*").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
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")
task.parseWhere(session, params)
return session.Count(task)
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
task.parseWhere(session, params)
list := make([]Task, 0)
err := session.GroupBy("t.id").Find(&list)
return int64(len(list)), err
}
// 解析where
func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
id, ok := params["Id"]
if ok && id.(int) > 0 {
session.And("t.id = ?", id)
}
hostId, ok := params["HostId"]
if ok && hostId.(int) > 0 {
session.And("host_id = ?", hostId)
}
name, ok := params["Name"]
if ok && name.(string) != "" {
session.And("t.name LIKE ?", "%" + name.(string) + "%")
}
protocol, ok := params["Protocol"]
if ok && protocol.(int) > 0 {
session.And("protocol = ?", protocol)
}
status, ok := params["Status"]
if ok && status.(int) > -1 {
session.And("status = ?", status)
}
}
func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
id, ok := params["Id"]
if ok && id.(int) > 0 {
session.And("t.id = ?", id)
}
hostId, ok := params["HostId"]
if ok && hostId.(int) > 0 {
session.And("th.host_id = ?", hostId)
}
name, ok := params["Name"]
if ok && name.(string) != "" {
session.And("t.name LIKE ?", "%"+name.(string)+"%")
}
protocol, ok := params["Protocol"]
if ok && protocol.(int) > 0 {
session.And("protocol = ?", protocol)
}
status, ok := params["Status"]
if ok && status.(int) > -1 {
session.And("status = ?", status)
}
func hostTableName() []string {
return []string{TablePrefix + "host", "host"}
}
tag, ok := params["Tag"]
if ok && tag.(string) != "" {
session.And("tag = ? ", tag)
}
}
+80
View File
@@ -0,0 +1,80 @@
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
}
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"
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
}
+64 -86
View File
@@ -1,120 +1,98 @@
package models
import (
"time"
"github.com/go-xorm/xorm"
"github.com/go-xorm/xorm"
"time"
)
type TaskType int8
// 任务执行日志
type TaskLog struct {
Id int64 `xorm:"bigint pk autoincr"`
TaskId int `xorm:"int notnull index default 0"` // 任务id
Name string `xorm:"varchar(32) notnull"` // 任务名称
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:ssh-command 3:系统命令
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 任务重试次数
Hostname string `xorm:"varchar(128) notnull defalut '' "` // SSH主机名,逗号分隔
StartTime time.Time `xorm:"datetime created"` // 开始执行时间
EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间
Status Status `xorm:"tinyint notnull default 1"` // 状态 0:执行失败 1:执行中 2:执行完毕 3:任务取消(上次任务未执行完成) 4:异步执行
NotifyId string `xorm:"varchar(32) notnull default '' "` // 回调通知ID
Result string `xorm:"mediumtext notnull defalut '' "` // 执行结果
TotalTime int `xorm:"-"` // 执行总时长
BaseModel `xorm:"-"`
Id int64 `xorm:"bigint pk autoincr"`
TaskId int `xorm:"int notnull index default 0"` // 任务id
Name string `xorm:"varchar(32) notnull"` // 任务名称
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:RPC
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 任务重试次数
Hostname string `xorm:"varchar(128) notnull defalut '' "` // RPC主机名,逗号分隔
StartTime time.Time `xorm:"datetime created"` // 开始执行时间
EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间
Status Status `xorm:"tinyint notnull index default 1"` // 状态 0:执行失败 1:执行中 2:执行完毕 3:任务取消(上次任务未执行完成) 4:异步执行
Result string `xorm:"mediumtext notnull defalut '' "` // 执行结果
TotalTime int `xorm:"-"` // 执行总时长
BaseModel `xorm:"-"`
}
func (taskLog *TaskLog) Create() (insertId int64, err error) {
_, err = Db.Insert(taskLog)
if err == nil {
insertId = taskLog.Id
}
_, err = Db.Insert(taskLog)
if err == nil {
insertId = taskLog.Id
}
return
return
}
// 更新
func (taskLog *TaskLog) Update(id int64, data CommonMap) (int64, error) {
return Db.Table(taskLog).ID(id).Update(data)
}
func (taskLog *TaskLog) UpdateStatus(notifyId string, status Status, result string) (int64, error) {
taskLog.Status = status
taskLog.Result = result
return Db.Cols("status,result").Where("notify_id = ?", notifyId).Update(taskLog)
}
func (taskLog *TaskLog) setStatus(id int64, status Status) (int64, error) {
return taskLog.Update(id, CommonMap{"status": status})
return Db.Table(taskLog).ID(id).Update(data)
}
func (taskLog *TaskLog) List(params CommonMap) ([]TaskLog, error) {
taskLog.parsePageAndPageSize(params)
list := make([]TaskLog, 0)
session := Db.Desc("id")
taskLog.parseWhere(session, params)
err := session.Limit(taskLog.PageSize, taskLog.pageLimitOffset()).Find(&list)
if len(list) > 0 {
for i, item := range list {
endTime := item.EndTime
if item.Status == Running {
endTime = time.Now()
}
execSeconds := endTime.Sub(item.StartTime).Seconds()
list[i].TotalTime = int(execSeconds)
}
}
taskLog.parsePageAndPageSize(params)
list := make([]TaskLog, 0)
session := Db.Desc("id")
taskLog.parseWhere(session, params)
err := session.Limit(taskLog.PageSize, taskLog.pageLimitOffset()).Find(&list)
if len(list) > 0 {
for i, item := range list {
endTime := item.EndTime
if item.Status == Running {
endTime = time.Now()
}
execSeconds := endTime.Sub(item.StartTime).Seconds()
list[i].TotalTime = int(execSeconds)
}
}
return list, err
}
// 根据通知ID获取任务ID
func (taskLog *TaskLog) GetTaskIdByNotifyId(notifyId string) (taskId int, err error) {
exist, err := Db.Where("notify_id = ?", notifyId).Get(taskLog)
if !exist || err != nil {
return
}
taskId = taskLog.TaskId
return
return list, err
}
// 清空表
func (taskLog *TaskLog) Clear() (int64, error) {
return Db.Where("1=1").Delete(taskLog);
func (taskLog *TaskLog) Clear() (int64, error) {
return Db.Where("1=1").Delete(taskLog)
}
// 删除N个月前的日志
func (taskLog *TaskLog) Remove(id int) (int64, error) {
t := time.Now().AddDate(0, -id, 0)
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
t := time.Now().AddDate(0, -id, 0)
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
}
func (taskLog *TaskLog) Total(params CommonMap) (int64, error) {
session := Db.NewSession()
taskLog.parseWhere(session, params)
return session.Count(taskLog)
session := Db.NewSession()
defer session.Close()
taskLog.parseWhere(session, params)
return session.Count(taskLog)
}
// 解析where
func (taskLog *TaskLog) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
taskId, ok := params["TaskId"]
if ok && taskId.(int) > 0 {
session.And("task_id = ?", taskId)
}
protocol, ok := params["Protocol"]
if ok && protocol.(int) > 0 {
session.And("protocol = ?", protocol)
}
status, ok := params["Status"]
if ok && status.(int) > -1 {
session.And("status = ?", status)
}
}
func (taskLog *TaskLog) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
taskId, ok := params["TaskId"]
if ok && taskId.(int) > 0 {
session.And("task_id = ?", taskId)
}
protocol, ok := params["Protocol"]
if ok && protocol.(int) > 0 {
session.And("protocol = ?", protocol)
}
status, ok := params["Status"]
if ok && status.(int) > -1 {
session.And("status = ?", status)
}
}
+65 -50
View File
@@ -1,110 +1,125 @@
package models
import (
"github.com/ouqiang/gocron/modules/utils"
"time"
"github.com/ouqiang/gocron/modules/utils"
"time"
)
const PasswordSaltLength = 6
// 用户model
type User struct {
Id int `xorm:"pk autoincr notnull "`
Name string `xorm:"varchar(32) notnull unique"` // 用户名
Password string `xorm:"char(32) notnull "` // 密码
Salt string `xorm:"char(6) notnull "` // 密码盐值
Email string `xorm:"varchar(50) notnull unique default '' "` // 邮箱
Created time.Time `xorm:"datetime notnull created"`
Updated time.Time `xorm:"datetime updated"`
Deleted time.Time `xorm:"datetime deleted"`
IsAdmin int8 `xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户
Status Status `xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用
BaseModel `xorm:"-"`
Id int `xorm:"pk autoincr notnull "`
Name string `xorm:"varchar(32) notnull unique"` // 用户名
Password string `xorm:"char(32) notnull "` // 密码
Salt string `xorm:"char(6) notnull "` // 密码盐值
Email string `xorm:"varchar(50) notnull unique default '' "` // 邮箱
Created time.Time `xorm:"datetime notnull created"`
Updated time.Time `xorm:"datetime updated"`
IsAdmin int8 `xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户
Status Status `xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用
BaseModel `xorm:"-"`
}
// 新增
func (user *User) Create() (insertId int, err error) {
user.Status = Enabled
user.Salt = user.generateSalt()
user.Password = user.encryptPassword(user.Password, user.Salt)
user.Status = Enabled
user.Salt = user.generateSalt()
user.Password = user.encryptPassword(user.Password, user.Salt)
_, err = Db.Insert(user)
if err == nil {
insertId = user.Id
}
_, err = Db.Insert(user)
if err == nil {
insertId = user.Id
}
return
return
}
// 更新
func (user *User) Update(id int, data CommonMap) (int64, error) {
return Db.Table(user).ID(id).Update(data)
return Db.Table(user).ID(id).Update(data)
}
func (user *User) UpdatePassword(id int, password string) (int64, error) {
salt := user.generateSalt()
safePassword := user.encryptPassword(password, salt)
func (user *User) UpdatePassword(id int, password string) (int64, error) {
salt := user.generateSalt()
safePassword := user.encryptPassword(password, salt)
return user.Update(id, CommonMap{"password": safePassword, "salt": salt})
return user.Update(id, CommonMap{"password": safePassword, "salt": salt})
}
// 删除
func (user *User) Delete(id int) (int64, error) {
return Db.Id(id).Delete(user)
return Db.Id(id).Delete(user)
}
// 禁用
func (user *User) Disable(id int) (int64, error) {
return user.Update(id, CommonMap{"status": Disabled})
return user.Update(id, CommonMap{"status": Disabled})
}
// 激活
func (user *User) Enable(id int) (int64, error) {
return user.Update(id, CommonMap{"status": Enabled})
return user.Update(id, CommonMap{"status": Enabled})
}
// 验证用户名和密码
func (user *User) Match(username, password string) bool {
where := "(name = ? OR email = ?)"
_, err := Db.Where(where, username, username).Get(user)
if err != nil {
return false
}
hashPassword := user.encryptPassword(password, user.Salt)
if hashPassword != user.Password {
return false
}
where := "(name = ? OR email = ?) AND status =? "
_, err := Db.Where(where, username, username, Enabled).Get(user)
if err != nil {
return false
}
hashPassword := user.encryptPassword(password, user.Salt)
if hashPassword != user.Password {
return false
}
return true
return true
}
// 获取用户详情
func (user *User) Find(id int) (error) {
_, err := Db.Id(id).Get(user)
return err
}
// 用户名是否存在
func (user *User) UsernameExists(username string) (int64, error) {
return Db.Where("name = ?", username).Count(user)
func (user *User) UsernameExists(username string, uid int) (int64, error) {
if uid > 0 {
return Db.Where("name = ? AND id != ?", username, uid).Count(user)
}
return Db.Where("name = ?", username).Count(user)
}
// 邮箱地址是否存在
func (user *User) EmailExists(email string) (int64, error) {
return Db.Where("email = ?", email).Count(user)
func (user *User) EmailExists(email string, uid int) (int64, error) {
if uid > 0 {
return Db.Where("email = ? AND id != ?", email, uid).Count(user)
}
return Db.Where("email = ?", email).Count(user)
}
func (user *User) List() ([]User, error) {
list := make([]User, 0)
err := Db.Desc("id").Find(&list)
func (user *User) List(params CommonMap) ([]User, error) {
user.parsePageAndPageSize(params)
list := make([]User, 0)
err := Db.Desc("id").Limit(user.PageSize, user.pageLimitOffset()).Find(&list)
return list, err
return list, err
}
func (user *User) Total() (int64, error) {
return Db.Count(user)
return Db.Count(user)
}
// 密码加密
func (user *User) encryptPassword(password, salt string) string {
return utils.Md5(password + salt)
return utils.Md5(password + salt)
}
// 生成密码盐值
func (user *User) generateSalt() string {
return utils.RandString(PasswordSaltLength)
return utils.RandString(PasswordSaltLength)
}
+94 -43
View File
@@ -1,65 +1,116 @@
package app
import (
"os"
"os"
"github.com/ouqiang/gocron/modules/logger"
"runtime"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/ini.v1"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/utils"
"io/ioutil"
"strconv"
"strings"
)
var (
AppDir string // 应用根目录
ConfDir string // 配置目录
LogDir string // 日志目录
DataDir string // 存放session等
AppConfig string // 应用配置文件
Installed bool // 应用是否安装过
Setting *ini.Section // 应用配置
AppDir string // 应用根目录
ConfDir string // 配置目录
LogDir string // 日志目录
DataDir string // 存放session等
AppConfig string // 应用配置文件
Installed bool // 应用是否安装过
Setting *setting.Setting // 应用配置
VersionId int // 版本号
VersionFile string // 版本号文件
)
func InitEnv() {
runtime.GOMAXPROCS(runtime.NumCPU())
logger.InitLogger()
wd, err := os.Getwd()
if err != nil {
logger.Fatal(err)
}
AppDir = wd
ConfDir = AppDir + "/conf"
LogDir = AppDir + "/log"
DataDir = AppDir + "/data"
AppConfig = ConfDir + "/app.ini"
checkDirExists(ConfDir, LogDir, DataDir)
Installed = IsInstalled()
func InitEnv(versionString string) {
logger.InitLogger()
wd, err := os.Getwd()
if err != nil {
logger.Fatal(err)
}
AppDir = wd
ConfDir = AppDir + "/conf"
LogDir = AppDir + "/log"
DataDir = AppDir + "/data"
AppConfig = ConfDir + "/app.ini"
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) {
return false
}
_, err := os.Stat(ConfDir + "/install.lock")
if os.IsNotExist(err) {
return false
}
return true
return true
}
// 创建安装锁文件
func CreateInstallLock() error {
_, err := os.Create(ConfDir + "/install.lock")
if err != nil {
logger.Error("创建安装锁文件conf/install.lock失败")
}
_, err := os.Create(ConfDir + "/install.lock")
if err != nil {
logger.Error("创建安装锁文件conf/install.lock失败")
}
return err
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 {
if !utils.FileExist(value) {
logger.Fatal(value + "目录不存在或无权限访问")
}
}
}
for _, value := range path {
if !utils.FileExist(value) {
logger.Fatal(value + "目录不存在或无权限访问")
}
}
}
+56 -56
View File
@@ -3,81 +3,81 @@ package httpclient
// http-client
import (
"io/ioutil"
"net/http"
"time"
"fmt"
"bytes"
"bytes"
"fmt"
"io/ioutil"
"net/http"
"time"
)
type ResponseWrapper struct {
StatusCode int
Body string
Header http.Header
type ResponseWrapper struct {
StatusCode int
Body string
Header http.Header
}
func Get(url string, timeout int) ResponseWrapper {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return createRequestError(err)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return createRequestError(err)
}
return request(req, timeout)
return request(req, timeout)
}
func PostParams(url string,params string, timeout int) ResponseWrapper {
buf := bytes.NewBufferString(params)
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return createRequestError(err)
}
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
func PostParams(url string, params string, timeout int) ResponseWrapper {
buf := bytes.NewBufferString(params)
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return createRequestError(err)
}
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
return request(req, timeout)
return request(req, timeout)
}
func PostJson(url string, body string, timeout int) ResponseWrapper {
buf := bytes.NewBufferString(body)
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return createRequestError(err)
}
req.Header.Set("Content-type", "application/json")
buf := bytes.NewBufferString(body)
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return createRequestError(err)
}
req.Header.Set("Content-type", "application/json")
return request(req, timeout)
return request(req, timeout)
}
func request(req *http.Request, timeout int) ResponseWrapper {
wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)}
client := &http.Client{}
if timeout > 0 {
client.Timeout = time.Duration(timeout) * time.Second
}
setRequestHeader(req)
resp, err := client.Do(req)
if err != nil {
wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error())
return wrapper
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())
return wrapper
}
wrapper.StatusCode = resp.StatusCode
wrapper.Body = string(body)
wrapper.Header = resp.Header
wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)}
client := &http.Client{}
if timeout > 0 {
client.Timeout = time.Duration(timeout) * time.Second
}
setRequestHeader(req)
resp, err := client.Do(req)
if err != nil {
wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error())
return wrapper
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())
return wrapper
}
wrapper.StatusCode = resp.StatusCode
wrapper.Body = string(body)
wrapper.Header = resp.Header
return wrapper
return wrapper
}
func setRequestHeader(req *http.Request) {
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 golang/gocron")
func setRequestHeader(req *http.Request) {
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 golang/gocron")
}
func createRequestError(err error) ResponseWrapper {
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
return ResponseWrapper{0, errorMessage, make(http.Header)}
}
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
return ResponseWrapper{0, errorMessage, make(http.Header)}
}
+78 -78
View File
@@ -1,11 +1,11 @@
package logger
import (
"github.com/cihub/seelog"
"gopkg.in/macaron.v1"
"fmt"
"os"
"runtime"
"fmt"
"github.com/cihub/seelog"
"gopkg.in/macaron.v1"
"os"
"runtime"
)
// 日志库
@@ -22,113 +22,113 @@ const (
FATAL
)
func InitLogger() {
config := getLogConfig()
l, err := seelog.LoggerFromConfigAsString(config)
if err != nil {
panic(err)
}
logger = l
func InitLogger() {
config := getLogConfig()
l, err := seelog.LoggerFromConfigAsString(config)
if err != nil {
panic(err)
}
logger = l
}
func Debug(v ...interface{}) {
if macaron.Env != macaron.DEV {
return
}
if macaron.Env != macaron.DEV {
return
}
write(DEBUG, v)
}
func Debugf(format string, v ...interface{}) {
if macaron.Env != macaron.DEV {
return
}
writef(DEBUG, format, v...)
func Debugf(format string, v ...interface{}) {
if macaron.Env != macaron.DEV {
return
}
writef(DEBUG, format, v...)
}
func Info(v ...interface{}) {
write(INFO, v)
}
func Infof(format string, v ...interface{}) {
writef(INFO, format, v...)
func Infof(format string, v ...interface{}) {
writef(INFO, format, v...)
}
func Warn(v ...interface{}) {
write(WARN, v)
}
func Warnf(format string, v ...interface{}) {
writef(WARN, format, v...)
func Warnf(format string, v ...interface{}) {
writef(WARN, format, v...)
}
func Error(v ...interface{}) {
write(ERROR, v)
}
func Errorf(format string, v ...interface{}) {
writef(ERROR, format, v...)
func Errorf(format string, v ...interface{}) {
writef(ERROR, format, v...)
}
func Fatal(v ...interface{}) {
write(FATAL, v)
}
func Fatalf(format string, v ...interface{}) {
writef(FATAL, format, v...)
func Fatalf(format string, v ...interface{}) {
writef(FATAL, format, v...)
}
func write(level Level, v ...interface{}) {
defer logger.Flush()
defer logger.Flush()
content := ""
if macaron.Env == macaron.DEV {
pc, file, line, ok := runtime.Caller(2)
if ok {
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
}
}
content := ""
if macaron.Env == macaron.DEV {
pc, file, line, ok := runtime.Caller(2)
if ok {
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
}
}
switch level {
case DEBUG:
logger.Debug(content, v)
case INFO:
logger.Info(content, v)
case WARN:
logger.Warn(content, v)
case FATAL:
logger.Critical(content, v)
os.Exit(1)
case ERROR:
logger.Error(content, v)
switch level {
case DEBUG:
logger.Debug(content, v)
case INFO:
logger.Info(content, v)
case WARN:
logger.Warn(content, v)
case FATAL:
logger.Critical(content, v)
os.Exit(1)
case ERROR:
logger.Error(content, v)
}
}
func writef(level Level, format string, v ...interface{}) {
defer logger.Flush()
func writef(level Level, format string, v ...interface{}) {
defer logger.Flush()
content := ""
if macaron.Env == macaron.DEV {
pc, file, line, ok := runtime.Caller(2)
if ok {
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
}
}
content := ""
if macaron.Env == macaron.DEV {
pc, file, line, ok := runtime.Caller(2)
if ok {
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
}
}
format = content + format
format = content + format
switch level {
case DEBUG:
logger.Debugf(format, v...)
case INFO:
logger.Infof(format, v...)
case WARN:
logger.Warnf(format, v...)
case FATAL:
logger.Criticalf(format, v...)
os.Exit(1)
case ERROR:
logger.Errorf(format, v...)
}
switch level {
case DEBUG:
logger.Debugf(format, v...)
case INFO:
logger.Infof(format, v...)
case WARN:
logger.Warnf(format, v...)
case FATAL:
logger.Criticalf(format, v...)
os.Exit(1)
case ERROR:
logger.Errorf(format, v...)
}
}
func getLogConfig() string {
@@ -145,16 +145,16 @@ func getLogConfig() string {
</formats>
</seelog>`
consoleConfig := ""
if macaron.Env == macaron.DEV {
consoleConfig =
`
consoleConfig := ""
if macaron.Env == macaron.DEV {
consoleConfig =
`
<filter levels="info,debug,critical,warn,error">
<console />
</filter>
`
}
config = fmt.Sprintf(config, consoleConfig)
}
config = fmt.Sprintf(config, consoleConfig)
return config
return config
}
+66 -68
View File
@@ -1,84 +1,82 @@
package notify
import (
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"strconv"
"strings"
"github.com/ouqiang/gocron/modules/utils"
"time"
"github.com/go-gomail/gomail"
"github.com/go-gomail/gomail"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"strconv"
"strings"
"time"
)
// @author qiang.ou<qingqianludao@gmail.com>
// @date 2017/5/1-00:19
type Mail struct {
}
func (mail *Mail) Send(msg Message) {
model := new(models.Setting)
mailSetting, err := model.Mail()
logger.Debugf("%+v", mailSetting)
if err != nil {
logger.Error("#mail#从数据库获取mail配置失败", err)
return
}
if mailSetting.Host == "" {
logger.Error("#mail#Host为空")
return
}
if mailSetting.Port == 0 {
logger.Error("#mail#Port为空")
return
}
if mailSetting.User == "" {
logger.Error("#mail#User为空")
return
}
if mailSetting.Password == "" {
logger.Error("#mail#Password为空")
return
}
toUsers := mail.getActiveMailUsers(mailSetting, msg)
mail.send(mailSetting, toUsers, msg)
func (mail *Mail) Send(msg Message) {
model := new(models.Setting)
mailSetting, err := model.Mail()
logger.Debugf("%+v", mailSetting)
if err != nil {
logger.Error("#mail#从数据库获取mail配置失败", err)
return
}
if mailSetting.Host == "" {
logger.Error("#mail#Host为空")
return
}
if mailSetting.Port == 0 {
logger.Error("#mail#Port为空")
return
}
if mailSetting.User == "" {
logger.Error("#mail#User为空")
return
}
if mailSetting.Password == "" {
logger.Error("#mail#Password为空")
return
}
toUsers := mail.getActiveMailUsers(mailSetting, msg)
mail.send(mailSetting, toUsers, msg)
}
func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message) {
body := msg["content"].(string)
body = strings.Replace(body, "\n", "<br>", -1)
gomailMessage := gomail.NewMessage()
gomailMessage.SetHeader("From", mailSetting.User)
gomailMessage.SetHeader("To", toUsers...)
gomailMessage.SetHeader("Subject", "gocron-定时任务监控通知")
gomailMessage.SetBody("text/html", body)
mailer := gomail.NewPlainDialer(mailSetting.Host, mailSetting.Port,
mailSetting.User, mailSetting.Password)
maxTimes := 3
i := 0
for i < maxTimes {
err := mailer.DialAndSend(gomailMessage)
if err == nil {
break;
}
i += 1
time.Sleep(2 * time.Second)
if i < maxTimes {
logger.Errorf("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
}
}
func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message) {
body := msg["content"].(string)
body = strings.Replace(body, "\n", "<br>", -1)
gomailMessage := gomail.NewMessage()
gomailMessage.SetHeader("From", mailSetting.User)
gomailMessage.SetHeader("To", toUsers...)
gomailMessage.SetHeader("Subject", "gocron-定时任务监控通知")
gomailMessage.SetBody("text/html", body)
mailer := gomail.NewPlainDialer(mailSetting.Host, mailSetting.Port,
mailSetting.User, mailSetting.Password)
maxTimes := 3
i := 0
for i < maxTimes {
err := mailer.DialAndSend(gomailMessage)
if err == nil {
break
}
i += 1
time.Sleep(2 * time.Second)
if i < maxTimes {
logger.Errorf("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
}
}
}
func (mail *Mail) getActiveMailUsers(mailSetting models.Mail, msg Message) []string {
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
users := []string{}
for _, v := range(mailSetting.MailUsers) {
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
users = append(users, v.Email)
}
}
func (mail *Mail) getActiveMailUsers(mailSetting models.Mail, msg Message) []string {
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
users := []string{}
for _, v := range mailSetting.MailUsers {
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
users = append(users, v.Email)
}
}
return users
}
return users
}
+33 -33
View File
@@ -1,52 +1,52 @@
package notify
import (
"time"
"github.com/ouqiang/gocron/modules/logger"
"fmt"
"fmt"
"github.com/ouqiang/gocron/modules/logger"
"time"
)
type Message map[string]interface{}
type Notifiable interface {
Send(msg Message)
Send(msg Message)
}
var queue chan Message = make(chan Message, 100)
func init() {
go run()
func init() {
go run()
}
// 把消息推入队列
func Push(msg Message) {
queue <- msg
queue <- msg
}
func run() {
for msg := range queue {
// 根据任务配置发送通知
taskType, taskTypeOk := msg["task_type"]
_, taskReceiverIdOk := msg["task_receiver_id"]
_, nameOk := msg["name"]
_, outputOk := msg["output"]
_, statusOk := msg["status"]
if !taskTypeOk || !taskReceiverIdOk || !nameOk || !outputOk || !statusOk {
logger.Errorf("#notify#参数不完整#%+v", msg)
continue
}
msg["content"] = fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", msg["name"], msg["status"], msg["output"])
logger.Debugf("%+v", msg)
switch(taskType.(int8)) {
case 1:
// 邮件
mail := Mail{}
go mail.Send(msg)
case 2:
// Slack
slack := Slack{}
go slack.Send(msg)
}
time.Sleep(1 * time.Second)
}
}
for msg := range queue {
// 根据任务配置发送通知
taskType, taskTypeOk := msg["task_type"]
_, taskReceiverIdOk := msg["task_receiver_id"]
_, nameOk := msg["name"]
_, outputOk := msg["output"]
_, statusOk := msg["status"]
if !taskTypeOk || !taskReceiverIdOk || !nameOk || !outputOk || !statusOk {
logger.Errorf("#notify#参数不完整#%+v", msg)
continue
}
msg["content"] = fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", msg["name"], msg["status"], msg["output"])
logger.Debugf("%+v", msg)
switch taskType.(int8) {
case 1:
// 邮件
mail := Mail{}
go mail.Send(msg)
case 2:
// Slack
slack := Slack{}
go slack.Send(msg)
}
time.Sleep(1 * time.Second)
}
}
+63 -62
View File
@@ -1,78 +1,79 @@
package notify
// 发送消息到slack
import (
"fmt"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"strings"
"github.com/ouqiang/gocron/models"
"strconv"
"time"
"fmt"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"strconv"
"strings"
"time"
)
type Slack struct {}
type Slack struct{}
func (slack *Slack) Send(msg Message) {
model := new(models.Setting)
slackSetting, err := model.Slack()
if err != nil {
logger.Error("#slack#从数据库获取slack配置失败", err)
return
}
if slackSetting.Url == "" {
logger.Error("#slack#webhook-url为空")
return
}
if len(slackSetting.Channels) == 0 {
logger.Error("#slack#channels配置为空")
return
}
logger.Debugf("%+v", slackSetting)
channels := slack.getActiveSlackChannels(slackSetting, msg)
logger.Debugf("%+v", channels)
for _, channel := range(channels) {
slack.send(msg, slackSetting.Url, channel)
}
func (slack *Slack) Send(msg Message) {
model := new(models.Setting)
slackSetting, err := model.Slack()
if err != nil {
logger.Error("#slack#从数据库获取slack配置失败", err)
return
}
if slackSetting.Url == "" {
logger.Error("#slack#webhook-url为空")
return
}
if len(slackSetting.Channels) == 0 {
logger.Error("#slack#channels配置为空")
return
}
logger.Debugf("%+v", slackSetting)
channels := slack.getActiveSlackChannels(slackSetting, msg)
logger.Debugf("%+v", channels)
for _, channel := range channels {
slack.send(msg, slackSetting.Url, channel)
}
}
func (slack *Slack) send(msg Message, slackUrl string, channel string) {
formatBody := slack.format(msg["content"].(string), channel)
timeout := 30
maxTimes := 3
i := 0
for i < maxTimes {
resp := httpclient.PostJson(slackUrl, formatBody, timeout)
if resp.StatusCode == 200 {
break;
}
i += 1
time.Sleep(2 * time.Second)
if i < maxTimes {
logger.Errorf("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
}
}
func (slack *Slack) send(msg Message, slackUrl string, channel string) {
formatBody := slack.format(msg["content"].(string), channel)
timeout := 30
maxTimes := 3
i := 0
for i < maxTimes {
resp := httpclient.PostJson(slackUrl, formatBody, timeout)
if resp.StatusCode == 200 {
break
}
i += 1
time.Sleep(2 * time.Second)
if i < maxTimes {
logger.Errorf("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
}
}
}
func (slack *Slack) getActiveSlackChannels(slackSetting models.Slack, msg Message) []string {
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
channels := []string{}
for _, v := range(slackSetting.Channels) {
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
channels = append(channels, v.Name)
}
}
func (slack *Slack) getActiveSlackChannels(slackSetting models.Slack, msg Message) []string {
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
channels := []string{}
for _, v := range slackSetting.Channels {
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
channels = append(channels, v.Name)
}
}
return channels
return channels
}
// 格式化消息内容
func (slack *Slack) format(content string, channel string) string {
content = utils.EscapeJson(content)
specialChars := []string{"&", "<", ">"}
replaceChars := []string{"&amp;", "&lt;", "&gt;"}
content = utils.ReplaceStrings(content, specialChars, replaceChars)
func (slack *Slack) format(content string, channel string) string {
content = utils.EscapeJson(content)
specialChars := []string{"&", "<", ">"}
replaceChars := []string{"&amp;", "&lt;", "&gt;"}
content = utils.ReplaceStrings(content, specialChars, replaceChars)
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
}
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
}
+69
View File
@@ -0,0 +1,69 @@
package auth
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"google.golang.org/grpc/credentials"
"io/ioutil"
)
type Certificate struct {
CAFile string
CertFile string
KeyFile string
ServerName string
}
func (c Certificate) GetTLSConfigForServer() (*tls.Config, error) {
certificate, err := tls.LoadX509KeyPair(
c.CertFile,
c.KeyFile,
)
certPool := x509.NewCertPool()
bs, err := ioutil.ReadFile(c.CAFile)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to read client ca cert: %s", err))
}
ok := certPool.AppendCertsFromPEM(bs)
if !ok {
return nil, errors.New("failed to append client certs")
}
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{certificate},
ClientCAs: certPool,
}
return tlsConfig, nil
}
func (c Certificate) GetTransportCredsForClient() (credentials.TransportCredentials, error) {
certificate, err := tls.LoadX509KeyPair(
c.CertFile,
c.KeyFile,
)
certPool := x509.NewCertPool()
bs, err := ioutil.ReadFile(c.CAFile)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to read ca cert: %s", err))
}
ok := certPool.AppendCertsFromPEM(bs)
if !ok {
return nil, errors.New("failed to append certs")
}
transportCreds := credentials.NewTLS(&tls.Config{
ServerName: c.ServerName,
Certificates: []tls.Certificate{certificate},
RootCAs: certPool,
})
return transportCreds, nil
}
+65
View File
@@ -0,0 +1,65 @@
package client
import (
"errors"
"fmt"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"time"
)
var (
errUnavailable = errors.New("无法连接远程服务器")
)
func Exec(ip string, port int, 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)
if err != nil {
return "", err
}
isConnClosed := false
defer func() {
if !isConnClosed {
grpcpool.Pool.Put(addr, conn)
}
}()
c := pb.NewTaskClient(conn)
if taskReq.Timeout <= 0 || taskReq.Timeout > 86400 {
taskReq.Timeout = 86400
}
timeout := time.Duration(taskReq.Timeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resp, err := c.Run(ctx, taskReq)
if err != nil {
return parseGRPCError(err, conn, &isConnClosed)
}
if resp.Error == "" {
return resp.Output, nil
}
return resp.Output, errors.New(resp.Error)
}
func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string, error) {
switch grpc.Code(err) {
case codes.Unavailable, codes.Internal:
conn.Close()
*connClosed = true
return "", errUnavailable
case codes.DeadlineExceeded:
return "", errors.New("执行超时, 强制结束")
}
return "", err
}
+139
View File
@@ -0,0 +1,139 @@
package grpcpool
import (
"errors"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/silenceper/pool"
"google.golang.org/grpc"
"strings"
"sync"
"time"
)
var (
Pool GRPCPool
)
var (
ErrInvalidConn = errors.New("invalid connection")
)
func init() {
Pool = GRPCPool{
make(map[string]pool.Pool),
sync.RWMutex{},
}
}
type GRPCPool struct {
// map key格式 ip:port
conns map[string]pool.Pool
sync.RWMutex
}
func (p *GRPCPool) Get(addr string) (*grpc.ClientConn, error) {
p.RLock()
pool, ok := p.conns[addr]
p.RUnlock()
if !ok {
err := p.newCommonPool(addr)
if err != nil {
return nil, err
}
}
p.RLock()
pool = p.conns[addr]
p.RUnlock()
conn, err := pool.Get()
if err != nil {
return nil, err
}
return conn.(*grpc.ClientConn), nil
}
func (p *GRPCPool) Put(addr string, conn *grpc.ClientConn) error {
p.RLock()
defer p.RUnlock()
pool, ok := p.conns[addr]
if ok {
return pool.Put(conn)
}
return ErrInvalidConn
}
// 释放连接池
func (p *GRPCPool) Release(addr string) {
p.Lock()
defer p.Unlock()
pool, ok := p.conns[addr]
if !ok {
return
}
pool.Release()
delete(p.conns, addr)
}
// 释放所有连接池
func (p *GRPCPool) ReleaseAll() {
p.Lock()
defer p.Unlock()
for _, pool := range p.conns {
pool.Release()
}
}
// 初始化底层连接池
func (p *GRPCPool) newCommonPool(addr string) error {
p.Lock()
defer p.Unlock()
commonPool, ok := p.conns[addr]
if ok {
return nil
}
poolConfig := &pool.PoolConfig{
InitialCap: 1,
MaxCap: 30,
Factory: func() (interface{}, error) {
if !app.Setting.EnableTLS {
return grpc.Dial(addr, grpc.WithInsecure())
}
server := strings.Split(addr, ":")
certificate := auth.Certificate{
CAFile: app.Setting.CAFile,
CertFile: app.Setting.CertFile,
KeyFile: app.Setting.KeyFile,
ServerName: server[0],
}
transportCreds, err := certificate.GetTransportCredsForClient()
if err != nil {
return nil, err
}
return grpc.Dial(addr, grpc.WithTransportCredentials(transportCreds))
},
Close: func(v interface{}) error {
conn, ok := v.(*grpc.ClientConn)
if ok && conn != nil {
return conn.Close()
}
return ErrInvalidConn
},
IdleTimeout: 3 * time.Minute,
}
commonPool, err := pool.NewChannelPool(poolConfig)
if err != nil {
return err
}
p.conns[addr] = commonPool
return nil
}
+176
View File
@@ -0,0 +1,176 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: task.proto
/*
Package rpc is a generated protocol buffer package.
It is generated from these files:
task.proto
It has these top-level messages:
TaskRequest
TaskResponse
*/
package rpc
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type TaskRequest struct {
Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
Timeout int32 `protobuf:"varint,3,opt,name=timeout" json:"timeout,omitempty"`
}
func (m *TaskRequest) Reset() { *m = TaskRequest{} }
func (m *TaskRequest) String() string { return proto.CompactTextString(m) }
func (*TaskRequest) ProtoMessage() {}
func (*TaskRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *TaskRequest) GetCommand() string {
if m != nil {
return m.Command
}
return ""
}
func (m *TaskRequest) GetTimeout() int32 {
if m != nil {
return m.Timeout
}
return 0
}
type TaskResponse struct {
Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
}
func (m *TaskResponse) Reset() { *m = TaskResponse{} }
func (m *TaskResponse) String() string { return proto.CompactTextString(m) }
func (*TaskResponse) ProtoMessage() {}
func (*TaskResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *TaskResponse) GetOutput() string {
if m != nil {
return m.Output
}
return ""
}
func (m *TaskResponse) GetError() string {
if m != nil {
return m.Error
}
return ""
}
func init() {
proto.RegisterType((*TaskRequest)(nil), "rpc.TaskRequest")
proto.RegisterType((*TaskResponse)(nil), "rpc.TaskResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for Task service
type TaskClient interface {
Run(ctx context.Context, in *TaskRequest, opts ...grpc.CallOption) (*TaskResponse, error)
}
type taskClient struct {
cc *grpc.ClientConn
}
func NewTaskClient(cc *grpc.ClientConn) TaskClient {
return &taskClient{cc}
}
func (c *taskClient) Run(ctx context.Context, in *TaskRequest, opts ...grpc.CallOption) (*TaskResponse, error) {
out := new(TaskResponse)
err := grpc.Invoke(ctx, "/rpc.Task/Run", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Task service
type TaskServer interface {
Run(context.Context, *TaskRequest) (*TaskResponse, error)
}
func RegisterTaskServer(s *grpc.Server, srv TaskServer) {
s.RegisterService(&_Task_serviceDesc, srv)
}
func _Task_Run_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Run(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpc.Task/Run",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Run(ctx, req.(*TaskRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Task_serviceDesc = grpc.ServiceDesc{
ServiceName: "rpc.Task",
HandlerType: (*TaskServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Run",
Handler: _Task_Run_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "task.proto",
}
func init() { proto.RegisterFile("task.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 170 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xb1, 0x0e, 0x82, 0x40,
0x0c, 0x86, 0x45, 0x04, 0x63, 0x75, 0xd0, 0xc6, 0x98, 0x8b, 0x13, 0x61, 0x62, 0x30, 0x0c, 0xea,
0xe8, 0xe2, 0x2b, 0x5c, 0x7c, 0x01, 0xc4, 0x1b, 0x0c, 0x81, 0x9e, 0xbd, 0xde, 0xfb, 0x1b, 0x38,
0x48, 0x18, 0xbf, 0x36, 0xfd, 0xfa, 0xff, 0x00, 0x52, 0xb9, 0xa6, 0xb4, 0x4c, 0x42, 0x18, 0xb3,
0xad, 0xf3, 0x27, 0x6c, 0x5f, 0x95, 0x6b, 0xb4, 0xf9, 0x79, 0xe3, 0x04, 0x15, 0xac, 0x6b, 0x6a,
0xdb, 0xaa, 0xfb, 0xa8, 0x65, 0x16, 0x15, 0x1b, 0x3d, 0x61, 0xbf, 0x91, 0x6f, 0x6b, 0xc8, 0x8b,
0x8a, 0xb3, 0xa8, 0x48, 0xf4, 0x84, 0xf9, 0x03, 0x76, 0x41, 0xe1, 0x2c, 0x75, 0xce, 0xe0, 0x09,
0x52, 0xf2, 0x62, 0xbd, 0xa8, 0x68, 0x50, 0x8c, 0x84, 0x47, 0x48, 0x0c, 0x33, 0xf1, 0x68, 0x0e,
0x70, 0xbd, 0xc3, 0xaa, 0xbf, 0xc6, 0x0b, 0xc4, 0xda, 0x77, 0xb8, 0x2f, 0xd9, 0xd6, 0xe5, 0x2c,
0xd2, 0xf9, 0x30, 0x9b, 0x84, 0x0f, 0xf9, 0xe2, 0x9d, 0x0e, 0x15, 0x6e, 0xff, 0x00, 0x00, 0x00,
0xff, 0xff, 0xef, 0x3c, 0x71, 0x6b, 0xd0, 0x00, 0x00, 0x00,
}
+17
View File
@@ -0,0 +1,17 @@
syntax = "proto3";
package rpc;
service Task {
rpc Run(TaskRequest) returns (TaskResponse) {}
}
message TaskRequest {
string command = 2; // 命令
int32 timeout = 3; // 任务执行超时时间
}
message TaskResponse {
string output = 1; // 命令标准输出
string error = 2; // 命令错误
}
+64
View File
@@ -0,0 +1,64 @@
package server
import (
"github.com/ouqiang/gocron/modules/rpc/auth"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"github.com/ouqiang/gocron/modules/utils"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"net"
)
type Server struct{}
func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
defer func() {
if err := recover(); err != nil {
grpclog.Println(err)
}
}()
output, err := utils.ExecShell(ctx, req.Command)
resp := new(pb.TaskResponse)
resp.Output = output
if err != nil {
resp.Error = err.Error()
} else {
resp.Error = ""
}
return resp, nil
}
func Start(addr string, enableTLS bool, certificate auth.Certificate) {
defer func() {
if err := recover(); err != nil {
grpclog.Println("panic", err)
}
}()
l, err := net.Listen("tcp", addr)
if err != nil {
grpclog.Fatal(err)
}
var s *grpc.Server
if enableTLS {
tlsConfig, err := certificate.GetTLSConfigForServer()
if err != nil {
grpclog.Fatal(err)
}
opt := grpc.Creds(credentials.NewTLS(tlsConfig))
s = grpc.NewServer(opt)
pb.RegisterTaskServer(s, Server{})
grpclog.Printf("listen %s with TLS", addr)
} else {
s = grpc.NewServer()
pb.RegisterTaskServer(s, Server{})
grpclog.Printf("listen %s", addr)
}
err = s.Serve(l)
grpclog.Fatal(err)
}
+96 -30
View File
@@ -1,45 +1,111 @@
package setting
import (
"errors"
"gopkg.in/ini.v1"
"errors"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/ini.v1"
)
const DefaultSection = "default"
// 读取配置
func Read(filename string) (*ini.Section,error) {
config, err := ini.Load(filename)
if err != nil {
return nil, err
}
section := config.Section(DefaultSection)
type Setting struct {
Db struct {
Engine string
Host string
Port int
User string
Password string
Database string
Prefix string
Charset string
MaxIdleConns int
MaxOpenConns int
}
AllowIps string
AppName string
ApiKey string
ApiSecret string
ApiSignEnable bool
return section, nil
EnableTLS bool
CAFile string
CertFile string
KeyFile string
}
// 读取配置
func Read(filename string) (*Setting, error) {
config, err := ini.Load(filename)
if err != nil {
return nil, err
}
section := config.Section(DefaultSection)
var s Setting
s.Db.Engine = section.Key("db.engine").MustString("mysql")
s.Db.Host = section.Key("db.host").MustString("127.0.0.1")
s.Db.Port = section.Key("db.port").MustInt(3306)
s.Db.User = section.Key("db.user").MustString("")
s.Db.Password = section.Key("db.password").MustString("")
s.Db.Database = section.Key("db.database").MustString("gocron")
s.Db.Prefix = section.Key("db.prefix").MustString("")
s.Db.Charset = section.Key("db.charset").MustString("utf8")
s.Db.MaxIdleConns = section.Key("db.max.idle.conns").MustInt(30)
s.Db.MaxOpenConns = section.Key("db.max.open.conns").MustInt(100)
s.AllowIps = section.Key("allow_ips").MustString("")
s.AppName = section.Key("app.name").MustString("定时任务管理系统")
s.ApiKey = section.Key("api.key").MustString("")
s.ApiSecret = section.Key("api.secret").MustString("")
s.ApiSignEnable = section.Key("api.sign.enable").MustBool(true)
s.EnableTLS = section.Key("enable_tls").MustBool(false)
s.CAFile = section.Key("ca_file").MustString("")
s.CertFile = section.Key("cert_file").MustString("")
s.KeyFile = section.Key("key_file").MustString("")
if s.EnableTLS {
if !utils.FileExist(s.CAFile) {
logger.Fatalf("failed to read ca cert file: %s", s.CAFile)
}
if !utils.FileExist(s.CertFile) {
logger.Fatalf("failed to read client cert file: %s", s.CertFile)
}
if !utils.FileExist(s.KeyFile) {
logger.Fatalf("failed to read client key file: %s", s.KeyFile)
}
}
return &s, nil
}
// 写入配置
func Write(config map[string]string, filename string) error {
if len(config) == 0 {
return errors.New("参数不能为空")
}
func Write(config []string, filename string) error {
if len(config) == 0 {
return errors.New("参数不能为空")
}
if len(config)%2 != 0 {
return errors.New("参数不匹配")
}
file := ini.Empty()
file := ini.Empty()
section, err := file.NewSection(DefaultSection)
if err != nil {
return err
}
for key, value := range config {
if key == "" {
continue
}
_, err = section.NewKey(key, value)
if err != nil {
return err
}
}
err = file.SaveTo(filename)
section, err := file.NewSection(DefaultSection)
if err != nil {
return err
}
for i := 0; i < len(config); {
_, err = section.NewKey(config[i], config[i+1])
if err != nil {
return err
}
i += 2
}
err = file.SaveTo(filename)
return err
return err
}
+103 -107
View File
@@ -1,143 +1,139 @@
package ssh
import (
"golang.org/x/crypto/ssh"
"fmt"
"net"
"time"
"errors"
"errors"
"fmt"
"golang.org/x/crypto/ssh"
"net"
"time"
)
type HostAuthType int8 // 认证方式
type HostAuthType int8 // 认证方式
const (
HostPassword = 1 // 密码认证
HostPublicKey = 2 // 公钥认证
HostPassword = 1 // 密码认证
HostPublicKey = 2 // 公钥认证
)
const SSHConnectTimeout = 10
type SSHConfig struct {
AuthType HostAuthType
User string
Password string
PrivateKey string
Host string
Port int
ExecTimeout int// 执行超时时间
type SSHConfig struct {
AuthType HostAuthType
User string
Password string
PrivateKey string
Host string
Port int
ExecTimeout int // 执行超时时间
}
type Result struct {
Output string
Err error
Output string
Err error
}
func parseSSHConfig(sshConfig SSHConfig) (config *ssh.ClientConfig, err error) {
timeout := time.Duration(SSHConnectTimeout) * time.Second
// 密码认证
if sshConfig.AuthType == HostPassword {
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.Password(sshConfig.Password),
},
Timeout: timeout,
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
timeout := time.Duration(SSHConnectTimeout) * time.Second
// 密码认证
if sshConfig.AuthType == HostPassword {
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.Password(sshConfig.Password),
},
Timeout: timeout,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return
}
return
}
signer, err := ssh.ParsePrivateKey([]byte(sshConfig.PrivateKey))
if err != nil {
return
}
signer, err := ssh.ParsePrivateKey([]byte(sshConfig.PrivateKey))
if err != nil {
return
}
// 公钥认证
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
Timeout: timeout,
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
// 公钥认证
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
Timeout: timeout,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return
return
}
// 执行shell命令
func Exec(sshConfig SSHConfig, cmd string) (output string, err error) {
client, err := getClient(sshConfig)
if err != nil {
return "", err
}
defer client.Close()
client, err := getClient(sshConfig)
if err != nil {
return "", err
}
defer client.Close()
session, err := client.NewSession()
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
if err != nil {
return "", err
}
defer session.Close()
// 后台运行
if sshConfig.ExecTimeout < 0 {
go session.CombinedOutput(cmd)
time.Sleep(10 * time.Second)
return "", nil
}
// 不限制超时
if sshConfig.ExecTimeout == 0 {
outputByte, execErr := session.CombinedOutput(cmd)
output = string(outputByte)
err = execErr
return
}
// 后台运行
if sshConfig.ExecTimeout < 0 {
go session.CombinedOutput(cmd)
time.Sleep(10 * time.Second)
return "", nil
}
// 不限制超时
if sshConfig.ExecTimeout == 0 {
outputByte, execErr := session.CombinedOutput(cmd)
output = string(outputByte)
err = execErr
return
}
var resultChan chan Result = make(chan Result)
var timeoutChan chan bool = make(chan bool)
go func() {
output, err := session.CombinedOutput(cmd)
resultChan <- Result{string(output), err}
}()
// todo 等待超时后,如何停止远程正在执行的任务, 使用timeout命令,但不具有通用性
go triggerTimeout(timeoutChan, sshConfig.ExecTimeout)
select {
case result := <- resultChan:
output = result.Output
err = result.Err
case <- timeoutChan:
output = ""
err = errors.New("timeout")
}
var resultChan chan Result = make(chan Result)
var timeoutChan chan bool = make(chan bool)
go func() {
output, err := session.CombinedOutput(cmd)
resultChan <- Result{string(output), err}
}()
// todo 等待超时后,如何停止远程正在执行的任务, 使用timeout命令,但不具有通用性
go triggerTimeout(timeoutChan, sshConfig.ExecTimeout)
select {
case result := <-resultChan:
output = result.Output
err = result.Err
case <-timeoutChan:
output = ""
err = errors.New("timeout")
}
return
return
}
func getClient(sshConfig SSHConfig) (*ssh.Client, error) {
config, err := parseSSHConfig(sshConfig)
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%s:%d", sshConfig.Host, sshConfig.Port)
func getClient(sshConfig SSHConfig) (*ssh.Client, error) {
config, err := parseSSHConfig(sshConfig)
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%s:%d", sshConfig.Host, sshConfig.Port)
return ssh.Dial("tcp", addr, config)
return ssh.Dial("tcp", addr, config)
}
func triggerTimeout(ch chan bool, timeout int){
// 最长执行时间不能超过24小时
if timeout <= 0 || timeout > 86400 {
timeout = 86400
}
time.Sleep(time.Duration(timeout) * time.Second)
close(ch)
func triggerTimeout(ch chan bool, timeout int) {
// 最长执行时间不能超过24小时
if timeout <= 0 || timeout > 86400 {
timeout = 86400
}
time.Sleep(time.Duration(timeout) * time.Second)
close(ch)
}
+28 -27
View File
@@ -1,16 +1,16 @@
package utils
import (
"encoding/json"
"github.com/ouqiang/gocron/modules/logger"
"encoding/json"
"github.com/ouqiang/gocron/modules/logger"
)
// json 格式输出
type response struct {
Code int `json:"code"` // 状态码 0:成功 非0:失败
Message string `json:"message"` // 信息
Data interface{} `json:"data"` // 数据
Code int `json:"code"` // 状态码 0:成功 非0:失败
Message string `json:"message"` // 信息
Data interface{} `json:"data"` // 数据
}
type JsonResponse struct{}
@@ -20,45 +20,46 @@ const ResponseFailure = 1
const NotFound = 2
const AuthError = 3
const ServerError = 4
const CaptchaError = 5
const SuccessContent = "操作成功"
const FailureContent = "操作失败"
func JsonResponseByErr(err error) string {
json := JsonResponse{}
if err != nil {
return json.CommonFailure(FailureContent, err)
}
json := JsonResponse{}
if err != nil {
return json.CommonFailure(FailureContent, err)
}
return json.Success(SuccessContent, nil)
return json.Success(SuccessContent, nil)
}
func (j *JsonResponse) Success(message string, data interface{}) string {
return j.response(ResponseSuccess, message, data)
return j.response(ResponseSuccess, message, data)
}
func (j *JsonResponse) Failure(code int, message string) string {
return j.response(code, message, nil)
return j.response(code, message, nil)
}
func (j *JsonResponse) CommonFailure(message string, err... error) string {
if len(err) > 0 {
logger.Warn(err)
}
return j.Failure(ResponseFailure, message)
func (j *JsonResponse) CommonFailure(message string, err ...error) string {
if len(err) > 0 {
logger.Warn(err)
}
return j.Failure(ResponseFailure, message)
}
func (j *JsonResponse) response(code int, message string, data interface{}) string {
resp := response{
Code: code,
Message: message,
Data: data,
}
resp := response{
Code: code,
Message: message,
Data: data,
}
result, err := json.Marshal(resp)
if err != nil {
logger.Error(err)
}
result, err := json.Marshal(resp)
if err != nil {
logger.Error(err)
}
return string(result)
return string(result)
}
+58 -58
View File
@@ -1,107 +1,107 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"math/rand"
"time"
"runtime"
"github.com/Tang-RoseChild/mahonia"
"strings"
"os"
"fmt"
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/Tang-RoseChild/mahonia"
"math/rand"
"os"
"runtime"
"strings"
"time"
)
// 生成长度为length的随机字符串
func RandString(length int64) string {
sources := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
result := []byte{}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
sourceLength := len(sources)
var i int64 = 0
for ; i < length; i++ {
result = append(result, sources[r.Intn(sourceLength)])
}
sources := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
result := []byte{}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
sourceLength := len(sources)
var i int64 = 0
for ; i < length; i++ {
result = append(result, sources[r.Intn(sourceLength)])
}
return string(result)
return string(result)
}
// 生成32位MD5摘要
func Md5(str string) string {
m := md5.New()
m.Write([]byte(str))
m := md5.New()
m.Write([]byte(str))
return hex.EncodeToString(m.Sum(nil))
return hex.EncodeToString(m.Sum(nil))
}
// 生成0-max之间随机数
func RandNumber(max int) int {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return r.Intn(max)
return r.Intn(max)
}
// 判断当前系统是否是windows
func IsWindows() bool {
return runtime.GOOS == "windows"
return runtime.GOOS == "windows"
}
// GBK编码转换为UTF8
func GBK2UTF8(s string) (string, bool) {
dec := mahonia.NewDecoder("gbk")
dec := mahonia.NewDecoder("gbk")
return dec.ConvertStringOK(s)
return dec.ConvertStringOK(s)
}
// 批量替换字符串
func ReplaceStrings(s string, old []string, replace []string) string {
if s == "" {
return s
}
if len(old) != len(replace) {
return s
}
func ReplaceStrings(s string, old []string, replace []string) string {
if s == "" {
return s
}
if len(old) != len(replace) {
return s
}
for i, v := range old {
s = strings.Replace(s, v, replace[i], 1000)
}
for i, v := range old {
s = strings.Replace(s, v, replace[i], 1000)
}
return s
return s
}
func InStringSlice(slice []string, element string) bool {
element = strings.TrimSpace(element)
for _, v := range slice {
if strings.TrimSpace(v) == element{
return true
}
}
element = strings.TrimSpace(element)
for _, v := range slice {
if strings.TrimSpace(v) == element {
return true
}
}
return false
return false
}
// 转义json特殊字符
func EscapeJson(s string) string {
specialChars := []string{"\\", "\b","\f", "\n", "\r", "\t", "\"",}
replaceChars := []string{ "\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\"",}
func EscapeJson(s string) string {
specialChars := []string{"\\", "\b", "\f", "\n", "\r", "\t", "\""}
replaceChars := []string{"\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\""}
return ReplaceStrings(s, specialChars, replaceChars)
return ReplaceStrings(s, specialChars, replaceChars)
}
// 判断文件是否存在及是否有权限访问
func FileExist(file string) bool {
_, err := os.Stat(file)
if os.IsNotExist(err) {
return false
}
if os.IsPermission(err) {
return false
}
_, err := os.Stat(file)
if os.IsNotExist(err) {
return false
}
if os.IsPermission(err) {
return false
}
return true
return true
}
// 格式化环境变量
func FormatUnixEnv(key, value string) string {
return fmt.Sprintf("export %s=%s; ", key, value)
}
return fmt.Sprintf("export %s=%s; ", key, value)
}
+12 -12
View File
@@ -3,22 +3,22 @@ package utils
import "testing"
func TestRandString(t *testing.T) {
str := RandString(32)
if len(str) != 32 {
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
}
str := RandString(32)
if len(str) != 32 {
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
}
}
func TestMd5(t *testing.T) {
str := Md5("123456")
if len(str) != 32 {
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
}
str := Md5("123456")
if len(str) != 32 {
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
}
}
func TestRandNumber(t *testing.T) {
num := RandNumber(10000)
if num <= 0 && num >= 10000 {
t.Fatalf("随机数不在有效范围内-%d", num)
}
num := RandNumber(10000)
if num <= 0 && num >= 10000 {
t.Fatalf("随机数不在有效范围内-%d", num)
}
}
+28 -35
View File
@@ -3,42 +3,35 @@
package utils
import (
"os/exec"
"syscall"
"time"
"fmt"
"errors"
"golang.org/x/net/context"
"os/exec"
"syscall"
)
// 执行shell命令,可设置执行超时时间
func ExecShellWithTimeout(timeout int, command string, args... string) (string, error) {
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
// 后台运行
if timeout == -1 {
go cmd.CombinedOutput()
return "", nil
}
// 不限制超时
if timeout == 0 {
output ,err := cmd.CombinedOutput()
return string(output), err
}
d := time.Duration(timeout) * time.Second
timer := time.AfterFunc(d, func() {
// 超时kill进程
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
})
output ,err := cmd.CombinedOutput()
timer.Stop()
return string(output), err
type Result struct {
output string
err error
}
// 格式化环境变量
func FormatEnv(key, value string) string {
return fmt.Sprintf("export %s=%s;", key, value)
}
// 执行shell命令,可设置执行超时时间
func ExecShell(ctx context.Context, command string) (string, error) {
cmd := exec.Command("/bin/bash", "-c", command)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
var resultChan chan Result = make(chan Result)
go func() {
output, err := cmd.CombinedOutput()
resultChan <- Result{string(output), err}
}()
select {
case <-ctx.Done():
if cmd.Process.Pid > 0 {
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
return "", errors.New("timeout killed")
case result := <-resultChan:
return result.output, result.err
}
}
+41 -35
View File
@@ -3,42 +3,48 @@
package utils
import (
"syscall"
"time"
"os/exec"
"strconv"
"fmt"
"errors"
"golang.org/x/net/context"
"os/exec"
"strconv"
"syscall"
)
// 执行shell命令,可设置执行超时时间
func ExecShellWithTimeout(timeout int, command string, args... string) (string, error) {
cmd := exec.Command(command, args...)
// 隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
// 后台运行
if timeout == -1 {
go cmd.CombinedOutput()
return "", nil
}
// 不限制超时
if timeout <= 0 {
output ,err := cmd.CombinedOutput()
return string(output), err
}
d := time.Duration(timeout) * time.Second
timer := time.AfterFunc(d, func() {
// 超时kill进程
exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
cmd.Process.Kill()
})
output ,err := cmd.CombinedOutput()
timer.Stop()
return string(output), err
type Result struct {
output string
err error
}
// 格式化环境变量
func FormatEnv(key, value string) string {
return fmt.Sprintf("set %s=%s & ", key, value)
}
// 执行shell命令,可设置执行超时时间
func ExecShell(ctx context.Context, command string) (string, error) {
cmd := exec.Command("cmd", "/C", command)
// 隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
var resultChan chan Result = make(chan Result)
go func() {
output, err := cmd.CombinedOutput()
resultChan <- Result{string(output), err}
}()
select {
case <-ctx.Done():
if cmd.Process.Pid > 0 {
exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
cmd.Process.Kill()
}
return "", errors.New("timeout killed")
case result := <-resultChan:
return ConvertEncoding(result.output), result.err
}
}
func ConvertEncoding(outputGBK string) string {
// windows平台编码为gbk,需转换为utf8才能入库
outputUTF8, ok := GBK2UTF8(outputGBK)
if ok {
return outputUTF8
}
return "命令输出转换编码失败(gbk to utf8)"
}
+8 -5
View File
@@ -10,13 +10,16 @@ function Util() {
swal("操作成功", '保存成功', 'success');
};
// ajax成功处理
util.ajaxSuccess = function(response, callback) {
util.ajaxSuccess = function(response, callback, failureCallback) {
if (response.code === undefined) {
swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error');
return;
}
if (response.code != SUCCESS) {
swal(FAILURE_MESSAGE, response.message ,'error');
if (failureCallback !== undefined) {
failureCallback(response.code, response.message);
}
return;
}
if (callback !== undefined) {
@@ -26,7 +29,7 @@ function Util() {
// ajax错误处理
util.ajaxFailure = function() {
// todo 错误处理
swal(FAILURE_MESSAGE, '未知错误', 'error');
swal(FAILURE_MESSAGE, '操作失败', 'error');
};
// get请求
util.get = function(url, callback) {
@@ -39,12 +42,12 @@ function Util() {
).error(util.ajaxFailure);
};
// post请求
util.post = function(url, params, callback) {
util.post = function(url, params, callback, failureCallback) {
$.post(
url,
util.objectTrim(params),
function(response) {
util.ajaxSuccess(response, callback);
util.ajaxSuccess(response, callback, failureCallback);
},
'json'
).error(util.ajaxFailure);
@@ -57,7 +60,7 @@ function Util() {
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
confirmButtonText: '删除',
confirmButtonText: '确定',
cancelButtonColor: '#d33',
cancelButtonText: "取消",
closeOnConfirm: false,
+15 -14
View File
@@ -1,20 +1,21 @@
package base
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/models"
"gopkg.in/macaron.v1"
)
func ParsePageAndPageSize(ctx *macaron.Context, params models.CommonMap) {
page := ctx.QueryInt("page")
pageSize := ctx.QueryInt("page_size")
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = models.PageSize
}
// ParsePageAndPageSize 解析查询参数中的页数和每页数量
func ParsePageAndPageSize(ctx *macaron.Context, params models.CommonMap) {
page := ctx.QueryInt("page")
pageSize := ctx.QueryInt("page_size")
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = models.PageSize
}
params["Page"] = page
params["PageSize"] = pageSize
}
params["Page"] = page
params["PageSize"] = pageSize
}
-110
View File
@@ -1,110 +0,0 @@
package delaytask
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/utils"
"strings"
"github.com/ouqiang/gocron/service"
"github.com/ouqiang/gocron/modules/logger"
"github.com/Unknwon/paginater"
"fmt"
"github.com/ouqiang/gocron/routers/base"
"html/template"
"github.com/ouqiang/gocron/modules/app"
)
func Index(ctx *macaron.Context) {
delayTaskModel := new(models.DelayTask)
queryParams := parseQueryParams(ctx)
total, err := delayTaskModel.Total(queryParams)
tasks, err := delayTaskModel.List(queryParams)
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("status=%d&page_size=%d",
queryParams["Status"], queryParams["PageSize"]);
queryParams["PageParams"] = template.URL(PageParams)
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
ctx.Data["Pagination"] = p
ctx.Data["Title"] = "延时任务列表"
ctx.Data["Tasks"] = tasks
ctx.Data["Params"] = queryParams
ctx.HTML(200, "task/delay_task")
}
func Create(ctx *macaron.Context) string {
url := ctx.QueryTrim("url")
params := ctx.QueryTrim("params")
delay := ctx.QueryInt("delay")
json := utils.JsonResponse{}
delayTaskEnabled, _ := app.Setting.Key("delay.task.enable").Bool()
if !delayTaskEnabled {
return json.CommonFailure("系统未开启延时任务")
}
if url == "" {
return json.CommonFailure("url地址不能为空")
}
lowerUrl := strings.ToLower(url)
if !strings.HasPrefix(lowerUrl, "http") &&
!strings.HasPrefix(lowerUrl, "https") {
return json.CommonFailure("无效的url地址")
}
if len(url) > 128 {
return json.CommonFailure("url长度不能超过128")
}
maxDelay := 1 << 31
if delay <= 0 || delay > maxDelay {
return json.CommonFailure("无效的delay, 取值范围1-(2^31-1)")
}
if len(params) > 256 {
return json.CommonFailure("params长度不能超过256")
}
delayTask := new(models.DelayTask)
delayTask.Url = url
delayTask.Params = params
delayTask.Delay = delay
delayTask.Status = models.Waiting
_, err := delayTask.Create()
if err != nil {
return json.CommonFailure("添加失败", err)
}
logger.Infof("新增延时任务#id-%d#url-%s#params-%s#delay-%d",
delayTask.Id, delayTask.Url, delayTask.Params, delayTask.Delay)
delayTaskService := new(service.DelayTask)
delayTaskService.Add(*delayTask)
return json.Success("添加成功", nil)
}
// 删除N个月前的日志
func Remove(ctx *macaron.Context) string {
month := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
if month < 1 || month > 12 {
return json.CommonFailure("参数取值范围1-12")
}
delayTaskModel := new(models.DelayTask)
_, err := delayTaskModel.Remove(month)
if err != nil {
return json.CommonFailure("删除失败", err)
}
return json.Success("删除成功", nil)
}
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
var params models.CommonMap = models.CommonMap{}
status := ctx.QueryInt("status")
if status >=0 {
status -= 1
}
params["Status"] = status
base.ParsePageAndPageSize(ctx, params)
return params
}
+2 -2
View File
@@ -3,6 +3,6 @@ package routers
import "gopkg.in/macaron.v1"
// 首页
func Home(ctx *macaron.Context) {
ctx.Redirect("/task")
func Home(ctx *macaron.Context) {
ctx.Redirect("/task")
}
+170 -163
View File
@@ -1,192 +1,199 @@
package host
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/logger"
"strconv"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/ouqiang/gocron/service"
"github.com/Unknwon/paginater"
"fmt"
"html/template"
"github.com/ouqiang/gocron/routers/base"
"github.com/go-macaron/binding"
"fmt"
"github.com/Unknwon/paginater"
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/rpc/client"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
"github.com/ouqiang/gocron/modules/rpc/proto"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"html/template"
"strconv"
"strings"
)
func Index(ctx *macaron.Context) {
hostModel := new(models.Host)
queryParams := parseQueryParams(ctx)
total, err := hostModel.Total(queryParams)
hosts, err := hostModel.List(queryParams)
if err != nil {
logger.Error(err)
}
name, ok := queryParams["name"].(string)
var safeNameHTML = ""
if ok {
safeNameHTML = template.HTMLEscapeString(name)
}
PageParams := fmt.Sprintf("id=%d&name=%s&page_size=%d",
queryParams["Id"], safeNameHTML, 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["Hosts"] = hosts
ctx.Data["Params"] = queryParams
ctx.HTML(200, "host/index")
// Index 主机列表
func Index(ctx *macaron.Context) {
hostModel := new(models.Host)
queryParams := parseQueryParams(ctx)
total, err := hostModel.Total(queryParams)
hosts, err := hostModel.List(queryParams)
if err != nil {
logger.Error(err)
}
name, ok := queryParams["name"].(string)
var safeNameHTML = ""
if ok {
safeNameHTML = template.HTMLEscapeString(name)
}
PageParams := fmt.Sprintf("id=%d&name=%s&page_size=%d",
queryParams["Id"], safeNameHTML, 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["Hosts"] = hosts
ctx.Data["Params"] = queryParams
ctx.HTML(200, "host/index")
}
func Create(ctx *macaron.Context) {
ctx.Data["Title"] = "添加主机"
ctx.HTML(200, "host/host_form")
// Create 创建主机页面
func Create(ctx *macaron.Context) {
ctx.Data["Title"] = "添加主机"
ctx.HTML(200, "host/host_form")
}
func Edit(ctx *macaron.Context) {
ctx.Data["Title"] = "编辑主机"
hostModel := new(models.Host)
id := ctx.ParamsInt(":id")
err := hostModel.Find(id)
if err != nil {
logger.Errorf("获取主机详情失败#主机id-%d", id)
}
ctx.Data["Host"] = hostModel
ctx.HTML(200, "host/host_form")
}
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)
}
sshConfig := ssh.SSHConfig{}
sshConfig.User = hostModel.Username
sshConfig.Host = hostModel.Name
sshConfig.Port = hostModel.Port
sshConfig.ExecTimeout = 5
sshConfig.AuthType = hostModel.AuthType
var password string
var privateKey string
if hostModel.AuthType == ssh.HostPassword {
password, err = hostModel.GetPasswordByHost(hostModel.Name)
if err != nil {
return json.CommonFailure(err.Error(), err)
}
sshConfig.Password = password
} else {
privateKey, err = hostModel.GetPrivateKeyByHost(hostModel.Name)
if err != nil {
return json.CommonFailure(err.Error(), err)
}
sshConfig.PrivateKey = privateKey
}
_, err = ssh.Exec(sshConfig, "pwd")
if err != nil {
return json.CommonFailure("连接失败-" + err.Error(), err)
}
return json.Success("连接成功", nil)
// Edit 修改主机页面
func Edit(ctx *macaron.Context) {
ctx.Data["Title"] = "编辑主机"
hostModel := new(models.Host)
id := ctx.ParamsInt(":id")
err := hostModel.Find(id)
if err != nil {
logger.Errorf("获取主机详情失败#主机id-%d", id)
}
ctx.Data["Host"] = hostModel
ctx.HTML(200, "host/host_form")
}
type HostForm struct {
Id int16
Name string `binding:"Required;MaxSize(64)"`
Alias string `binding:"Required;MaxSize(32)"`
Username string `binding:"Required;MaxSize(32)"`
Port int `binding:"Required;Range(1-65535)"`
AuthType ssh.HostAuthType `binding:"Required:Range(1,2)"`
Remark string
Id int16
Name string `binding:"Required;MaxSize(64)"`
Alias string `binding:"Required;MaxSize(32)"`
Port int `binding:"Required;Range(1-65535)"`
Remark string
}
// Error 表单验证错误处理
func (f HostForm) Error(ctx *macaron.Context, errs binding.Errors) {
if len(errs) == 0 {
return
}
json := utils.JsonResponse{}
content := json.CommonFailure("表单验证失败, 请检测输入")
if len(errs) == 0 {
return
}
json := utils.JsonResponse{}
content := json.CommonFailure("表单验证失败, 请检测输入")
ctx.Resp.Write([]byte(content))
ctx.Resp.Write([]byte(content))
}
func Store(ctx *macaron.Context, form HostForm) string {
json := utils.JsonResponse{}
hostModel := new(models.Host)
id := form.Id
nameExist, err := hostModel.NameExists(form.Name, form.Id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
if nameExist {
return json.CommonFailure("主机名已存在")
}
// Store 保存、修改主机信息
func Store(ctx *macaron.Context, form HostForm) string {
json := utils.JsonResponse{}
hostModel := new(models.Host)
id := form.Id
nameExist, err := hostModel.NameExists(form.Name, form.Id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
if nameExist {
return json.CommonFailure("主机名已存在")
}
hostModel.Name = form.Name
hostModel.Alias = form.Alias
hostModel.Username = form.Username
hostModel.Port = form.Port
hostModel.Remark = form.Remark
hostModel.AuthType = form.AuthType
isCreate := false
if id > 0 {
_, err = hostModel.UpdateBean(id)
} else {
isCreate = true
id, err = hostModel.Create()
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
hostModel.Name = strings.TrimSpace(form.Name)
hostModel.Alias = strings.TrimSpace(form.Alias)
hostModel.Port = form.Port
hostModel.Remark = strings.TrimSpace(form.Remark)
isCreate := false
oldHostModel := new(models.Host)
err = oldHostModel.Find(int(id))
if err != nil {
return json.CommonFailure("主机不存在")
}
taskModel := new(models.TaskHost)
tasks, err := taskModel.ActiveListByHostId(id)
if err != nil {
return json.CommonFailure("刷新任务主机信息失败", err)
}
if !isCreate && len(tasks) > 0 {
serviceTask := new(service.Task)
serviceTask.BatchAdd(tasks)
}
if id > 0 {
_, err = hostModel.UpdateBean(id)
} else {
isCreate = true
id, err = hostModel.Create()
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
return json.Success("保存成功", nil)
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)
}
taskModel := new(models.Task)
tasks, err := taskModel.ActiveListByHostId(id)
if err != nil {
return json.CommonFailure("刷新任务主机信息失败", err)
}
serviceTask := new(service.Task)
serviceTask.BatchAdd(tasks)
}
return json.Success("保存成功", nil)
}
func Remove(ctx *macaron.Context) string {
id, err := strconv.Atoi(ctx.Params(":id"))
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("参数错误", err)
}
taskModel := new(models.Task)
exist,err := taskModel.HostIdExist(int16(id))
if err != nil {
return json.CommonFailure("操作失败", err)
}
if exist {
return json.CommonFailure("有任务引用此主机,不能删除")
}
// Remove 删除主机
func Remove(ctx *macaron.Context) string {
id, err := strconv.Atoi(ctx.Params(":id"))
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("参数错误", err)
}
taskHostModel := new(models.TaskHost)
exist, err := taskHostModel.HostIdExist(int16(id))
if err != nil {
return json.CommonFailure("操作失败", err)
}
if exist {
return json.CommonFailure("有任务引用此主机,不能删除")
}
hostModel := new(models.Host)
_, err =hostModel.Delete(id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
hostModel := new(models.Host)
err = hostModel.Find(int(id))
if err != nil {
return json.CommonFailure("主机不存在")
}
return json.Success("操作成功", nil)
_, err = hostModel.Delete(id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
addr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
grpcpool.Pool.Release(addr)
return json.Success("操作成功", nil)
}
// Ping 测试主机是否可连接
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, 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{}
params["Id"] = ctx.QueryInt("id")
params["Name"] = ctx.QueryTrim("name")
base.ParsePageAndPageSize(ctx, params)
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
var params models.CommonMap = models.CommonMap{}
params["Id"] = ctx.QueryInt("id")
params["Name"] = ctx.QueryTrim("name")
base.ParsePageAndPageSize(ctx, params)
return params
}
return params
}
+121 -113
View File
@@ -1,155 +1,163 @@
package install
import (
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/macaron.v1"
"strconv"
"fmt"
"github.com/ouqiang/gocron/service"
"github.com/go-macaron/binding"
"fmt"
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"strconv"
)
// 系统安装
type InstallForm struct {
DbType string `binding:"In(mysql)"`
DbHost string `binding:"Required;MaxSize(50)"`
DbPort int `binding:"Required;Range(1,65535)"`
DbUsername string `binding:"Required;MaxSize(50)"`
DbPassword string `binding:"Required;MaxSize(30)"`
DbName string `binding:"Required;MaxSize(50)"`
DbTablePrefix string `binding:"MaxSize(20)"`
AdminUsername string `binding:"Required;MinSize(3)"`
AdminPassword string `binding:"Required;MinSize(6)"`
ConfirmAdminPassword string `binding:"Required;MinSize(6)"`
AdminEmail string `binding:"Required;Email;MaxSize(50)"`
DbType string `binding:"In(mysql)"`
DbHost string `binding:"Required;MaxSize(50)"`
DbPort int `binding:"Required;Range(1,65535)"`
DbUsername string `binding:"Required;MaxSize(50)"`
DbPassword string `binding:"Required;MaxSize(30)"`
DbName string `binding:"Required;MaxSize(50)"`
DbTablePrefix string `binding:"MaxSize(20)"`
AdminUsername string `binding:"Required;MinSize(3)"`
AdminPassword string `binding:"Required;MinSize(6)"`
ConfirmAdminPassword string `binding:"Required;MinSize(6)"`
AdminEmail string `binding:"Required;Email;MaxSize(50)"`
}
func (f InstallForm) Error(ctx *macaron.Context, errs binding.Errors) {
if len(errs) == 0 {
return
}
json := utils.JsonResponse{}
content := json.CommonFailure("表单验证失败, 请检测输入")
if len(errs) == 0 {
return
}
json := utils.JsonResponse{}
content := json.CommonFailure("表单验证失败, 请检测输入")
ctx.Resp.Write([]byte(content))
ctx.Resp.Write([]byte(content))
}
func Create(ctx *macaron.Context) {
if app.Installed {
ctx.Redirect("/")
}
ctx.Data["Title"] = "安装"
ctx.Data["DisableNav"] = true
ctx.HTML(200, "install/create")
if app.Installed {
ctx.Redirect("/")
}
ctx.Data["Title"] = "安装"
ctx.Data["DisableNav"] = true
ctx.HTML(200, "install/create")
}
// 安装
func Store(ctx *macaron.Context, form InstallForm) string {
json := utils.JsonResponse{}
if app.Installed {
return json.CommonFailure("系统已安装!")
}
if form.AdminPassword != form.ConfirmAdminPassword {
return json.CommonFailure("两次输入密码不匹配")
}
err := testDbConnection(form)
if err != nil {
return json.CommonFailure("数据库连接失败", err)
}
// 写入数据库配置
err = writeConfig(form)
if err != nil {
return json.CommonFailure("数据库配置写入文件失败", err)
}
json := utils.JsonResponse{}
if app.Installed {
return json.CommonFailure("系统已安装!")
}
if form.AdminPassword != form.ConfirmAdminPassword {
return json.CommonFailure("两次输入密码不匹配")
}
err := testDbConnection(form)
if err != nil {
return json.CommonFailure("数据库连接失败", err)
}
// 写入数据库配置
err = writeConfig(form)
if err != nil {
return json.CommonFailure("数据库配置写入文件失败", err)
}
appConfig, err := setting.Read(app.AppConfig)
if err != nil {
return json.CommonFailure("读取应用配置失败", err)
}
app.Setting = appConfig
appConfig, err := setting.Read(app.AppConfig)
if err != nil {
return json.CommonFailure("读取应用配置失败", err)
}
app.Setting = appConfig
models.Db = models.CreateDb()
// 创建数据库表
migration := new(models.Migration)
err = migration.Exec(form.DbName)
if err != nil {
return json.CommonFailure(fmt.Sprintf("创建数据库表失败-%s", err.Error()), err)
}
models.Db = models.CreateDb()
// 创建数据库表
migration := new(models.Migration)
err = migration.Install(form.DbName)
if err != nil {
return json.CommonFailure(fmt.Sprintf("创建数据库表失败-%s", err.Error()), err)
}
// 创建管理员账号
err = createAdminUser(form)
if err != nil {
return json.CommonFailure("创建管理员账号失败", err)
}
// 创建管理员账号
err = createAdminUser(form)
if err != nil {
return json.CommonFailure("创建管理员账号失败", err)
}
// 创建安装锁
err = app.CreateInstallLock()
if err != nil {
return json.CommonFailure("创建文件安装锁失败", err)
}
// 创建安装锁
err = app.CreateInstallLock()
if err != nil {
return json.CommonFailure("创建文件安装锁失败", err)
}
app.Installed = true
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
// 更新版本号文件
app.UpdateVersionFile()
return json.Success("安装成功", nil)
app.Installed = true
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
return json.Success("安装成功", nil)
}
// 配置写入文件
func writeConfig(form InstallForm) error {
dbConfig := map[string]string{
"db.engine": form.DbType,
"db.host": form.DbHost,
"db.port": strconv.Itoa(form.DbPort),
"db.user": form.DbUsername,
"db.password": form.DbPassword,
"db.database": form.DbName,
"db.prefix": form.DbTablePrefix,
"db.charset": "utf8",
"allow_ips" : "",
"app.name": "定时任务管理系统", // 应用名称
"delay.task.enable": "false", // 是否开启延时任务
"delay.task.slots": "3600", // 时间轮槽数量
"delay.task.tick": "1s", // 时间轮每次转动的时间
}
dbConfig := []string{
"db.engine", form.DbType,
"db.host", form.DbHost,
"db.port", strconv.Itoa(form.DbPort),
"db.user", form.DbUsername,
"db.password", form.DbPassword,
"db.database", form.DbName,
"db.prefix", form.DbTablePrefix,
"db.charset", "utf8",
"db.max.idle.conns", "30",
"db.max.open.conns", "100",
"allow_ips", "",
"app.name", "定时任务管理系统", // 应用名称
"api.key", "",
"api.secret", "",
"enable_tls", "false",
"ca_file", "",
"cert_file", "",
"key_file", "",
}
return setting.Write(dbConfig, app.AppConfig)
return setting.Write(dbConfig, app.AppConfig)
}
// 创建管理员账号
func createAdminUser(form InstallForm) error {
user := new(models.User)
user.Name = form.AdminUsername
user.Password = form.AdminPassword
user.Email = form.AdminEmail
user.IsAdmin = 1
_, err := user.Create()
user := new(models.User)
user.Name = form.AdminUsername
user.Password = form.AdminPassword
user.Email = form.AdminEmail
user.IsAdmin = 1
_, err := user.Create()
return err
return err
}
// 测试数据库连接
func testDbConnection(form InstallForm) error {
var dbConfig map[string]string = make(map[string]string)
dbConfig["engine"] = form.DbType
dbConfig["host"] = form.DbHost
dbConfig["port"] = strconv.Itoa(form.DbPort)
dbConfig["user"] = form.DbUsername
dbConfig["password"] = form.DbPassword
dbConfig["charset"] = "utf8"
db, err := models.CreateTmpDb(dbConfig)
if err != nil {
return err
}
var s setting.Setting
s.Db.Engine = form.DbType
s.Db.Host = form.DbHost
s.Db.Port = form.DbPort
s.Db.User = form.DbUsername
s.Db.Password = form.DbPassword
s.Db.Charset = "utf8"
db, err := models.CreateTmpDb(&s)
if err != nil {
return err
}
defer db.Close()
err = db.Ping()
defer db.Close()
err = db.Ping()
return err
return err
}
}
+25 -25
View File
@@ -1,30 +1,30 @@
package loginlog
import (
"gopkg.in/macaron.v1"
"github.com/Unknwon/paginater"
"fmt"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/routers/base"
"html/template"
"fmt"
"github.com/Unknwon/paginater"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/routers/base"
"gopkg.in/macaron.v1"
"html/template"
)
func Index(ctx *macaron.Context) {
loginLogModel := new(models.LoginLog)
params := models.CommonMap{}
base.ParsePageAndPageSize(ctx, params)
total, err := loginLogModel.Total()
loginLogs, err := loginLogModel.List(params)
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("page_size=%d", params["PageSize"]);
params["PageParams"] = template.URL(PageParams)
p := paginater.New(int(total), params["PageSize"].(int), params["Page"].(int), 5)
ctx.Data["Pagination"] = p
ctx.Data["Title"] = "登录日志"
ctx.Data["LoginLogs"] = loginLogs
ctx.Data["Params"] = params
ctx.HTML(200, "manage/login_log")
}
func Index(ctx *macaron.Context) {
loginLogModel := new(models.LoginLog)
params := models.CommonMap{}
base.ParsePageAndPageSize(ctx, params)
total, err := loginLogModel.Total()
loginLogs, err := loginLogModel.List(params)
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("page_size=%d", params["PageSize"])
params["PageParams"] = template.URL(PageParams)
p := paginater.New(int(total), params["PageSize"].(int), params["Page"].(int), 5)
ctx.Data["Pagination"] = p
ctx.Data["Title"] = "登录日志"
ctx.Data["LoginLogs"] = loginLogs
ctx.Data["Params"] = params
ctx.HTML(200, "manage/login_log")
}
+83 -85
View File
@@ -1,138 +1,136 @@
package manage
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"encoding/json"
"encoding/json"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/macaron.v1"
)
// region slack
func EditSlack(ctx *macaron.Context) {
ctx.Data["Title"] = "Slack配置"
settingModel := new(models.Setting)
slack, err := settingModel.Slack()
if err != nil {
logger.Error(err)
}
ctx.Data["Slack"] = slack
ctx.HTML(200, "manage/slack")
func EditSlack(ctx *macaron.Context) {
ctx.Data["Title"] = "Slack配置"
settingModel := new(models.Setting)
slack, err := settingModel.Slack()
if err != nil {
logger.Error(err)
}
ctx.Data["Slack"] = slack
ctx.HTML(200, "manage/slack")
}
func Slack(ctx *macaron.Context) string {
settingModel := new(models.Setting)
slack, err := settingModel.Slack()
if err != nil {
logger.Error(err)
}
json := utils.JsonResponse{}
settingModel := new(models.Setting)
slack, err := settingModel.Slack()
if err != nil {
logger.Error(err)
}
json := utils.JsonResponse{}
return json.Success("", slack)
return json.Success("", slack)
}
func UpdateSlackUrl(ctx *macaron.Context) string {
url := ctx.QueryTrim("url")
settingModel := new(models.Setting)
_, err := settingModel.UpdateSlackUrl(url)
url := ctx.QueryTrim("url")
settingModel := new(models.Setting)
_, err := settingModel.UpdateSlackUrl(url)
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
func CreateSlackChannel(ctx *macaron.Context) string {
channel := ctx.QueryTrim("channel")
settingModel := new(models.Setting)
if settingModel.IsChannelExist(channel) {
json := utils.JsonResponse{}
func CreateSlackChannel(ctx *macaron.Context) string {
channel := ctx.QueryTrim("channel")
settingModel := new(models.Setting)
if settingModel.IsChannelExist(channel) {
json := utils.JsonResponse{}
return json.CommonFailure("Channel已存在")
}
_, err := settingModel.CreateChannel(channel)
return json.CommonFailure("Channel已存在")
}
_, err := settingModel.CreateChannel(channel)
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
func RemoveSlackChannel(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
settingModel := new(models.Setting)
_, err := settingModel.RemoveChannel(id)
func RemoveSlackChannel(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
settingModel := new(models.Setting)
_, err := settingModel.RemoveChannel(id)
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
// endregion
// region 邮件
func EditMail(ctx *macaron.Context) {
ctx.Data["Title"] = "邮件配置"
settingModel := new(models.Setting)
mail, err := settingModel.Mail()
if err != nil {
logger.Error(err)
}
ctx.Data["Mail"] = mail
ctx.HTML(200, "manage/mail")
func EditMail(ctx *macaron.Context) {
ctx.Data["Title"] = "邮件配置"
settingModel := new(models.Setting)
mail, err := settingModel.Mail()
if err != nil {
logger.Error(err)
}
ctx.Data["Mail"] = mail
ctx.HTML(200, "manage/mail")
}
func Mail(ctx *macaron.Context) string {
settingModel := new(models.Setting)
mail, err := settingModel.Mail()
if err != nil {
logger.Error(err)
}
settingModel := new(models.Setting)
mail, err := settingModel.Mail()
if err != nil {
logger.Error(err)
}
json := utils.JsonResponse{}
json := utils.JsonResponse{}
return json.Success("", mail)
return json.Success("", mail)
}
type MailServerForm struct {
Host string `binding:"Required;MaxSize(100)"`
Port int `binding:"Required;Range(1-65535)"`
User string `binding:"Required;MaxSize(64);Email"`
Password string `binding:"Required;MaxSize(64)"`
Host string `binding:"Required;MaxSize(100)"`
Port int `binding:"Required;Range(1-65535)"`
User string `binding:"Required;MaxSize(64);Email"`
Password string `binding:"Required;MaxSize(64)"`
}
func UpdateMailServer(ctx *macaron.Context, form MailServerForm) string {
jsonByte, _ := json.Marshal(form)
settingModel := new(models.Setting)
_, err := settingModel.UpdateMailServer(string(jsonByte))
jsonByte, _ := json.Marshal(form)
settingModel := new(models.Setting)
_, err := settingModel.UpdateMailServer(string(jsonByte))
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
func ClearMailServer(ctx *macaron.Context) string {
jsonByte, _ := json.Marshal(MailServerForm{})
settingModel := new(models.Setting)
_, err := settingModel.UpdateMailServer(string(jsonByte))
jsonByte, _ := json.Marshal(MailServerForm{})
settingModel := new(models.Setting)
_, err := settingModel.UpdateMailServer(string(jsonByte))
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
func CreateMailUser(ctx *macaron.Context) string {
username := ctx.QueryTrim("username")
email := ctx.QueryTrim("email")
settingModel := new(models.Setting)
if username == "" || email == "" {
json := utils.JsonResponse{}
func CreateMailUser(ctx *macaron.Context) string {
username := ctx.QueryTrim("username")
email := ctx.QueryTrim("email")
settingModel := new(models.Setting)
if username == "" || email == "" {
json := utils.JsonResponse{}
return json.CommonFailure("用户名、邮箱均不能为空")
}
_, err := settingModel.CreateMailUser(username, email)
return json.CommonFailure("用户名、邮箱均不能为空")
}
_, err := settingModel.CreateMailUser(username, email)
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
func RemoveMailUser(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
settingModel := new(models.Setting)
_, err := settingModel.RemoveMailUser(id)
func RemoveMailUser(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
settingModel := new(models.Setting)
_, err := settingModel.RemoveMailUser(id)
return utils.JsonResponseByErr(err)
return utils.JsonResponseByErr(err)
}
// endregion
// endregion
+287 -201
View File
@@ -1,23 +1,27 @@
package routers
import (
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/routers/install"
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/routers/task"
"github.com/ouqiang/gocron/routers/host"
"github.com/ouqiang/gocron/routers/tasklog"
"github.com/ouqiang/gocron/modules/utils"
"github.com/go-macaron/session"
"github.com/go-macaron/toolbox"
"strings"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/routers/user"
"github.com/go-macaron/gzip"
"github.com/ouqiang/gocron/routers/manage"
"github.com/ouqiang/gocron/routers/loginlog"
"github.com/ouqiang/gocron/routers/delaytask"
"github.com/go-macaron/binding"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
"github.com/go-macaron/gzip"
"github.com/go-macaron/session"
"github.com/go-macaron/toolbox"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/host"
"github.com/ouqiang/gocron/routers/install"
"github.com/ouqiang/gocron/routers/loginlog"
"github.com/ouqiang/gocron/routers/manage"
"github.com/ouqiang/gocron/routers/task"
"github.com/ouqiang/gocron/routers/tasklog"
"github.com/ouqiang/gocron/routers/user"
"gopkg.in/macaron.v1"
"html/template"
"strconv"
"strings"
"time"
)
// 静态文件目录
@@ -25,217 +29,299 @@ const StaticDir = "public"
// 路由注册
func Register(m *macaron.Macaron) {
// 所有GET方法,自动注册HEAD方法
m.SetAutoHead(true)
// 首页
m.Get("/", Home)
// 系统安装
m.Group("/install", func() {
m.Get("", install.Create)
m.Post("/store", binding.Bind(install.InstallForm{}), install.Store)
})
// 所有GET方法,自动注册HEAD方法
m.SetAutoHead(true)
// 首页
m.Get("/", Home)
// 系统安装
m.Group("/install", func() {
m.Get("", install.Create)
m.Post("/store", binding.Bind(install.InstallForm{}), install.Store)
})
// 用户
m.Group("/user", func() {
m.Get("/login", user.Login)
m.Post("/login", user.ValidateLogin)
m.Get("/logout", user.Logout)
m.Get("/editPassword", user.EditPassword)
m.Post("/editPassword", user.UpdatePassword)
})
// 用户
m.Group("/user", func() {
m.Get("", user.Index)
m.Get("/create", user.Create)
m.Get("/edit/:id", user.Edit)
m.Post("/store", binding.Bind(user.UserForm{}), user.Store)
m.Post("/remove/:id", user.Remove)
m.Get("/login", user.Login)
m.Post("/login", user.ValidateLogin)
m.Get("/logout", user.Logout)
m.Post("/enable/:id", user.Enable)
m.Post("/disable/:id", user.Disable)
m.Get("/editMyPassword", user.EditMyPassword)
m.Post("/editMyPassword", user.UpdateMyPassword)
m.Get("/editPassword/:id", user.EditPassword)
m.Post("/editPassword/:id", user.UpdatePassword)
})
// 定时任务
m.Group("/task", func() {
m.Get("/create", task.Create)
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
m.Get("/edit/:id", task.Edit)
m.Get("", task.Index)
m.Get("/log", tasklog.Index)
m.Post("/log/clear", tasklog.Clear)
m.Post("/remove/:id", task.Remove)
m.Post("/enable/:id", task.Enable)
m.Post("/disable/:id", task.Disable)
m.Get("/run/:id", task.Run)
})
// 定时任务
m.Group("/task", func() {
m.Get("/create", task.Create)
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
m.Get("/edit/:id", task.Edit)
m.Get("", task.Index)
m.Get("/log", tasklog.Index)
m.Post("/log/clear", tasklog.Clear)
m.Post("/remove/:id", task.Remove)
m.Post("/enable/:id", task.Enable)
m.Post("/disable/:id", task.Disable)
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)
})
// 主机
m.Group("/host", func() {
m.Get("/create", host.Create)
m.Get("/edit/:id", host.Edit)
m.Get("/ping/:id", host.Ping)
m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
m.Get("", host.Index)
m.Post("/remove/:id", host.Remove)
})
// 管理
m.Group("/manage", func() {
m.Group("/slack", func() {
m.Get("/", manage.Slack)
m.Get("/edit", manage.EditSlack)
m.Post("/url", manage.UpdateSlackUrl)
m.Post("/channel", manage.CreateSlackChannel)
m.Post("/channel/remove/:id", manage.RemoveSlackChannel)
})
m.Group("/mail", func() {
m.Get("/", manage.Mail)
m.Get("/edit", manage.EditMail)
m.Post("/server", binding.Bind(manage.MailServerForm{}), manage.UpdateMailServer)
m.Post("/server/clear", manage.ClearMailServer)
m.Post("/user", manage.CreateMailUser)
m.Post("/user/remove/:id", manage.RemoveMailUser)
})
m.Get("/login-log", loginlog.Index)
})
// 管理
m.Group("/manage", func() {
m.Group("/slack", func() {
m.Get("/", manage.Slack)
m.Get("/edit", manage.EditSlack)
m.Post("/url", manage.UpdateSlackUrl)
m.Post("/channel", manage.CreateSlackChannel)
m.Post("/channel/remove/:id", manage.RemoveSlackChannel)
})
m.Group("/mail", func() {
m.Get("/", manage.Mail)
m.Get("/edit", manage.EditMail)
m.Post("/server", binding.Bind(manage.MailServerForm{}), manage.UpdateMailServer)
m.Post("/server/clear", manage.ClearMailServer)
m.Post("/user", manage.CreateMailUser)
m.Post("/user/remove/:id", manage.RemoveMailUser)
})
m.Get("/login-log", loginlog.Index)
})
// API
m.Group("/api/v1", func() {
m.Post("/tasklog/remove/:id", tasklog.Remove)
m.Post("/task/enable/:id", task.Enable)
m.Post("/task/disable/:id", task.Disable)
}, apiAuth)
// API
m.Group("/api/v1", func() {
m.Route("/tasklog/update-status", "GET,POST", tasklog.UpdateStatus)
m.Post("/tasklog/remove/:id", tasklog.Remove)
m.Post("/delaytask/push", delaytask.Create)
m.Post("/delaytask/log/remove/:id", delaytask.Remove)
});
// 404错误
m.NotFound(func(ctx *macaron.Context) {
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "404 - NOT FOUND"
ctx.HTML(404, "error/404")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.NotFound, "您访问的地址不存在")))
}
})
// 50x错误
m.InternalServerError(func(ctx *macaron.Context) {
logger.Debug("500错误")
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "500 - INTERNAL SERVER ERROR"
ctx.HTML(500, "error/500")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.ServerError, "网站暂时无法访问,请稍后再试")))
}
})
// 404错误
m.NotFound(func(ctx *macaron.Context) {
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "404 - NOT FOUND"
ctx.HTML(404, "error/404")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.NotFound, "您访问的地址不存在")))
}
})
// 50x错误
m.InternalServerError(func(ctx *macaron.Context) {
logger.Debug("500错误")
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "500 - INTERNAL SERVER ERROR"
ctx.HTML(500, "error/500")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.ServerError, "网站暂时无法访问,请稍后再试")))
}
})
}
// 中间件注册
func RegisterMiddleware(m *macaron.Macaron) {
m.Use(macaron.Logger())
m.Use(macaron.Recovery())
if macaron.Env != macaron.DEV {
m.Use(gzip.Gziper())
}
m.Use(macaron.Static(StaticDir))
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "templates",
Extensions: []string{".html"},
// 模板语法分隔符,默认为 ["{{", "}}"]
Delims: macaron.Delims{"{{{", "}}}"},
// 追加的 Content-Type 头信息,默认为 "UTF-8"
Charset: "UTF-8",
// 渲染具有缩进格式的 JSON,默认为不缩进
IndentJSON: true,
// 渲染具有缩进格式的 XML,默认为不缩进
IndentXML: true,
}))
m.Use(session.Sessioner(session.Options{
Provider: "file",
ProviderConfig: app.DataDir + "/sessions",
}))
m.Use(toolbox.Toolboxer(m))
checkAppInstall(m)
m.Use(func(ctx *macaron.Context, sess session.Store){
if app.Installed {
ipAuth(ctx)
userAuth(ctx, sess)
setShareData(ctx, sess)
}
})
m.Use(macaron.Logger())
m.Use(macaron.Recovery())
if macaron.Env != macaron.DEV {
m.Use(gzip.Gziper())
}
m.Use(macaron.Static(StaticDir))
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "templates",
Extensions: []string{".html"},
// 模板语法分隔符,默认为 ["{{", "}}"]
Delims: macaron.Delims{"{{{", "}}}"},
// 追加的 Content-Type 头信息,默认为 "UTF-8"
Charset: "UTF-8",
// 渲染具有缩进格式的 JSON,默认为不缩进
IndentJSON: true,
// 渲染具有缩进格式的 XML,默认为不缩进
IndentXML: true,
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",
}))
m.Use(toolbox.Toolboxer(m))
checkAppInstall(m)
m.Use(func(ctx *macaron.Context, sess session.Store) {
if app.Installed {
ipAuth(ctx)
userAuth(ctx, sess)
urlAuth(ctx, sess)
setShareData(ctx, sess)
}
})
}
// region 自定义中间件
/** 系统未安装,重定向到安装页面 **/
func checkAppInstall(m *macaron.Macaron) {
m.Use(func(ctx *macaron.Context) {
installUrl := "/install"
if strings.HasPrefix(ctx.Req.URL.Path, installUrl) {
return
}
if !app.Installed {
ctx.Redirect(installUrl)
}
})
func checkAppInstall(m *macaron.Macaron) {
m.Use(func(ctx *macaron.Context) {
installUrl := "/install"
if strings.HasPrefix(ctx.Req.URL.Path, installUrl) {
return
}
if !app.Installed {
ctx.Redirect(installUrl)
}
})
}
// IP验证, 通过反向代理访问gocron,需设置Header X-Real-IP才能获取到客户端真实IP
func ipAuth(ctx *macaron.Context) {
allowIpsStr := app.Setting.Key("allow_ips").String()
if allowIpsStr == "" {
return
}
clientIp := ctx.RemoteAddr()
allowIps := strings.Split(allowIpsStr, ",")
if !utils.InStringSlice(allowIps, clientIp) {
logger.Warnf("非法IP访问-%s", clientIp)
ctx.Status(403)
}
func ipAuth(ctx *macaron.Context) {
allowIpsStr := app.Setting.AllowIps
if allowIpsStr == "" {
return
}
clientIp := ctx.RemoteAddr()
allowIps := strings.Split(allowIpsStr, ",")
if !utils.InStringSlice(allowIps, clientIp) {
logger.Warnf("非法IP访问-%s", clientIp)
ctx.Status(403)
}
}
// 用户认证
func userAuth(ctx *macaron.Context, sess session.Store) {
if user.IsLogin(sess) {
return
}
uri := ctx.Req.URL.Path
found := false
excludePaths := []string{"/install", "/user/login", "/api"}
for _, path := range excludePaths {
if strings.HasPrefix(uri, path) {
found = true
break
}
}
if !found {
ctx.Redirect("/user/login")
}
func userAuth(ctx *macaron.Context, sess session.Store) {
if user.IsLogin(sess) {
return
}
uri := ctx.Req.URL.Path
found := false
excludePaths := []string{"/install", "/user/login", "/api"}
for _, path := range excludePaths {
if strings.HasPrefix(uri, path) {
found = true
break
}
}
if !found {
ctx.Redirect("/user/login")
}
}
// URL权限验证
func urlAuth(ctx *macaron.Context, sess session.Store) {
if user.IsAdmin(sess) {
return
}
// 普通用户允许访问的URL地址
allowPaths := []string{
"",
"/task",
"/task/log",
"/host",
"/user/login",
"/user/logout",
"/user/editMyPassword",
}
uri := strings.TrimSpace(ctx.Req.URL.Path)
uri = strings.TrimRight(uri, "/")
for _, path := range allowPaths {
if path == uri {
return
}
}
ctx.Status(403)
}
/** 设置共享数据 **/
func setShareData(ctx *macaron.Context, sess session.Store) {
ctx.Data["URI"] = ctx.Req.URL.Path
urlPath := strings.TrimPrefix(ctx.Req.URL.Path, "/")
paths := strings.Split(urlPath, "/")
ctx.Data["Controller"] = ""
ctx.Data["Action"] = ""
if len(paths) > 0 {
ctx.Data["Controller"] = paths[0]
}
if len(paths) > 1 {
ctx.Data["Action"] = paths[1]
}
ctx.Data["LoginUsername"] = user.Username(sess)
ctx.Data["LoginUid"] = user.Uid(sess)
ctx.Data["IsAdmin"] = user.IsAdmin(sess)
ctx.Data["AppName"] = app.Setting.AppName
}
/** API接口签名验证 **/
func apiAuth(ctx *macaron.Context) {
if !app.Setting.ApiSignEnable {
return
}
apiKey := strings.TrimSpace(app.Setting.ApiKey)
apiSecret := strings.TrimSpace(app.Setting.ApiSecret)
json := utils.JsonResponse{}
if apiKey == "" || apiSecret == "" {
msg := json.CommonFailure("使用API前, 请先配置密钥")
ctx.Write([]byte(msg))
return
}
currentTimestamp := time.Now().Unix()
time := ctx.QueryInt64("time")
if time <= 0 {
msg := json.CommonFailure("参数time不能为空")
ctx.Write([]byte(msg))
return
}
if time < (currentTimestamp - 1800) {
msg := json.CommonFailure("time无效")
ctx.Write([]byte(msg))
return
}
sign := ctx.QueryTrim("sign")
if sign == "" {
msg := json.CommonFailure("参数sign不能为空")
ctx.Write([]byte(msg))
return
}
raw := apiKey + strconv.FormatInt(time, 10) + strings.TrimSpace(ctx.Req.URL.Path) + apiSecret
realSign := utils.Md5(raw)
if sign != realSign {
msg := json.CommonFailure("签名验证失败")
ctx.Write([]byte(msg))
return
}
}
// endregion
/** 设置共享数据 **/
func setShareData(ctx *macaron.Context, sess session.Store) {
ctx.Data["URI"] = ctx.Req.URL.Path
urlPath := strings.TrimPrefix(ctx.Req.URL.Path, "/")
paths := strings.Split(urlPath, "/")
ctx.Data["Controller"] = ""
ctx.Data["Action"] = ""
if len(paths) > 0 {
ctx.Data["Controller"] = paths[0]
}
if len(paths) > 1 {
ctx.Data["Action"] = paths[1]
}
ctx.Data["LoginUsername"] = user.Username(sess)
ctx.Data["LoginUid"] = user.Uid(sess)
ctx.Data["AppName"] = app.Setting.Key("app.name").String()
}
func isAjaxRequest(ctx *macaron.Context) bool {
req := ctx.Req.Header.Get("X-Requested-With")
if req == "XMLHttpRequest" {
return true
}
req := ctx.Req.Header.Get("X-Requested-With")
if req == "XMLHttpRequest" {
return true
}
return false
return false
}
func isGetRequest(ctx *macaron.Context) bool {
return ctx.Req.Method == "GET"
}
return ctx.Req.Method == "GET"
}
+269 -209
View File
@@ -1,274 +1,334 @@
package task
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/service"
"strconv"
"github.com/jakecoffman/cron"
"github.com/Unknwon/paginater"
"fmt"
"html/template"
"github.com/ouqiang/gocron/routers/base"
"github.com/go-macaron/binding"
"strings"
"fmt"
"github.com/Unknwon/paginater"
"github.com/go-macaron/binding"
"github.com/jakecoffman/cron"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"html/template"
"strconv"
"strings"
)
type TaskForm struct {
Id int
Name string `binding:"Required;MaxSize(32)"`
Spec string `binding:"Required;MaxSize(64)"`
Protocol models.TaskProtocol `binding:"In(1,2,3)"`
Command string `binding:"Required;MaxSize(256)"`
Timeout int `binding:"Range(-1,86400)"`
Multi int8 `binding:"In(1,2)"`
RetryTimes int8
HostId int16
Remark string
NotifyStatus int8 `binding:In(1,2,3)`
NotifyType int8 `binding:In(1,2)`
NotifyReceiverId string
Id int
Level models.TaskLevel `binding:"Required;In(1,2)"`
DependencyStatus models.TaskDependencyStatus
DependencyTaskId string
Name string `binding:"Required;MaxSize(32)"`
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 string
Tag string
Remark string
NotifyStatus int8 `binding:"In(1,2,3)"`
NotifyType int8 `binding:"In(1,2,3)"`
NotifyReceiverId string
}
func (f TaskForm) Error(ctx *macaron.Context, errs binding.Errors) {
if len(errs) == 0 {
return
}
json := utils.JsonResponse{}
content := json.CommonFailure("表单验证失败, 请检测输入")
if len(errs) == 0 {
return
}
json := utils.JsonResponse{}
content := json.CommonFailure("表单验证失败, 请检测输入")
ctx.Resp.Write([]byte(content))
ctx.Resp.Write([]byte(content))
}
// 首页
func Index(ctx *macaron.Context) {
taskModel := new(models.Task)
queryParams := parseQueryParams(ctx)
total, err := taskModel.Total(queryParams)
if err != nil {
logger.Error(err)
}
tasks, err := taskModel.List(queryParams)
if err != nil {
logger.Error(err)
}
name, ok := queryParams["name"].(string)
var safeNameHTML = ""
if ok {
safeNameHTML = template.HTMLEscapeString(name)
}
PageParams := fmt.Sprintf("id=%d&host_id=%d&name=%s&protocol=%d&status=%d&page_size=%d",
queryParams["Id"], queryParams["HostId"], safeNameHTML, queryParams["Protocol"], 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
setHostsToTemplate(ctx)
ctx.Data["Params"] = queryParams
ctx.Data["Title"] = "任务列表"
ctx.Data["Tasks"] = tasks
ctx.HTML(200, "task/index")
func Index(ctx *macaron.Context) {
taskModel := new(models.Task)
queryParams := parseQueryParams(ctx)
total, err := taskModel.Total(queryParams)
if err != nil {
logger.Error(err)
}
tasks, err := taskModel.List(queryParams)
if err != nil {
logger.Error(err)
}
name, ok := queryParams["name"].(string)
var safeNameHTML = ""
if ok {
safeNameHTML = template.HTMLEscapeString(name)
}
PageParams := fmt.Sprintf("id=%d&host_id=%d&name=%s&protocol=%d&tag=%s&status=%d&page_size=%d",
queryParams["Id"], queryParams["HostId"], safeNameHTML, queryParams["Protocol"], queryParams["Tag"], 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
setHostsToTemplate(ctx)
ctx.Data["Params"] = queryParams
ctx.Data["Title"] = "任务列表"
ctx.Data["Tasks"] = tasks
ctx.HTML(200, "task/index")
}
// 新增页面
func Create(ctx *macaron.Context) {
setHostsToTemplate(ctx)
ctx.Data["Title"] = "添加任务"
ctx.HTML(200, "task/task_form")
func Create(ctx *macaron.Context) {
setHostsToTemplate(ctx)
ctx.Data["Title"] = "添加任务"
ctx.HTML(200, "task/task_form")
}
// 编辑页面
func Edit(ctx *macaron.Context) {
id := ctx.ParamsInt(":id")
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil || task.Id != id {
logger.Errorf("编辑任务#获取任务详情失败#任务ID-%d#%s", id, err.Error())
ctx.Redirect("/task")
}
ctx.Data["Task"] = task
ctx.Data["Title"] = "编辑"
setHostsToTemplate(ctx)
ctx.HTML(200, "task/task_form")
func Edit(ctx *macaron.Context) {
id := ctx.ParamsInt(":id")
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil || task.Id != id {
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"] = "编辑"
ctx.HTML(200, "task/task_form")
}
// 保存任务
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)
}
if nameExists {
return json.CommonFailure("任务名称已存在")
}
func Store(ctx *macaron.Context, form TaskForm) string {
json := utils.JsonResponse{}
taskModel := models.Task{}
var id int = form.Id
nameExists, err := taskModel.NameExist(form.Name, form.Id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if nameExists {
return json.CommonFailure("任务名称已存在")
}
if form.Protocol == models.TaskSSH && form.HostId <= 0 {
return json.CommonFailure("请选择主机名")
}
if form.Protocol == models.TaskRPC && form.HostId == "" {
return json.CommonFailure("请选择主机名")
}
if form.Protocol != models.TaskHTTP {
taskModel.HostId = form.HostId
} else {
taskModel.HostId = 0
}
taskModel.Name = form.Name
taskModel.Protocol = form.Protocol
taskModel.Command = form.Command
taskModel.Timeout = form.Timeout
taskModel.Remark = form.Remark
taskModel.Multi = form.Multi
taskModel.RetryTimes = form.RetryTimes
if taskModel.Multi != 1 {
taskModel.Multi = 0
}
taskModel.NotifyStatus = form.NotifyStatus - 1
taskModel.NotifyType = form.NotifyType - 1
taskModel.NotifyReceiverId = form.NotifyReceiverId
taskModel.Spec = form.Spec
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
return json.CommonFailure("请至少选择一个接收者")
}
if taskModel.Protocol == models.TaskHTTP {
command := strings.ToLower(taskModel.Command)
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
return json.CommonFailure("请输入正确的URL地址")
}
if taskModel.Timeout == -1 {
return json.CommonFailure("HTTP任务不支持后台运行")
}
if taskModel.Timeout > 300 {
return json.CommonFailure("HTTP任务超时时间不能超过300秒")
}
}
taskModel.Name = form.Name
taskModel.Protocol = form.Protocol
taskModel.Command = form.Command
taskModel.Timeout = form.Timeout
taskModel.Tag = form.Tag
taskModel.Remark = form.Remark
taskModel.Multi = form.Multi
taskModel.RetryTimes = form.RetryTimes
if taskModel.Multi != 1 {
taskModel.Multi = 0
}
taskModel.NotifyStatus = form.NotifyStatus - 1
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("至少选择一个通知接收者")
}
if taskModel.Protocol == models.TaskHTTP {
command := strings.ToLower(taskModel.Command)
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
return json.CommonFailure("请输入正确的URL地址")
}
if taskModel.Timeout > 300 {
return json.CommonFailure("HTTP任务超时时间不能超过300秒")
}
}
if taskModel.RetryTimes > 10 || taskModel.RetryTimes < 0 {
return json.CommonFailure("任务重试次数取值0-10")
}
if taskModel.RetryTimes > 10 || taskModel.RetryTimes < 0 {
return json.CommonFailure("任务重试次数取值0-10")
}
if taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
taskModel.DependencyStatus != models.TaskDependencyStatusWeak {
return json.CommonFailure("请选择依赖关系")
}
if taskModel.Protocol != models.TaskSSH {
taskModel.HostId = 0
}
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 {
id, err = taskModel.Create()
} else {
_, err = taskModel.UpdateBean(id)
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
if id > 0 && taskModel.DependencyTaskId != "" {
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
return json.CommonFailure("不允许设置当前任务为子任务")
}
}
return json.Success("保存成功", nil)
if id == 0 {
// 任务添加后开始调度执行
taskModel.Status = models.Running
id, err = taskModel.Create()
} else {
_, err = taskModel.UpdateBean(id)
}
if err != nil {
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, _ := taskModel.GetStatus(id)
if status == models.Enabled && taskModel.Level == models.TaskLevelParent {
addTaskToTimer(id)
}
return json.Success("保存成功", nil)
}
// 删除任务
func Remove(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
taskModel := new(models.Task)
_, err := taskModel.Delete(id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
taskModel := new(models.Task)
_, err := taskModel.Delete(id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
service.Cron.RemoveJob(strconv.Itoa(id))
taskHostModel := new(models.TaskHost)
taskHostModel.Remove(id)
return json.Success(utils.SuccessContent, nil)
service.Cron.RemoveJob(strconv.Itoa(id))
return json.Success(utils.SuccessContent, nil)
}
// 激活任务
func Enable(ctx *macaron.Context) string {
return changeStatus(ctx, models.Enabled)
return changeStatus(ctx, models.Enabled)
}
// 暂停任务
func Disable(ctx *macaron.Context) string {
return changeStatus(ctx, models.Disabled)
return changeStatus(ctx, models.Disabled)
}
// 手动运行任务
func Run(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
taskModel := new(models.Task)
task , err := taskModel.Detail(id)
if err != nil || task.Id <= 0 {
return json.CommonFailure("获取任务详情失败", err)
}
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil || task.Id <= 0 {
return json.CommonFailure("获取任务详情失败", err)
}
task.Spec = "手动运行"
serviceTask := new(service.Task)
serviceTask.Run(task)
task.Spec = "手动运行"
serviceTask := new(service.Task)
serviceTask.Run(task)
return json.Success("任务已开始运行, 请到任务日志中查看结果", nil);
return json.Success("任务已开始运行, 请到任务日志中查看结果", nil)
}
// 改变任务状态
func changeStatus(ctx *macaron.Context, status models.Status) string {
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
taskModel := new(models.Task)
_, err := taskModel.Update(id, models.CommonMap{
"Status": status,
})
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
taskModel := new(models.Task)
_, err := taskModel.Update(id, models.CommonMap{
"Status": status,
})
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if status == models.Enabled {
addTaskToTimer(id)
} else {
service.Cron.RemoveJob(strconv.Itoa(id))
}
if status == models.Enabled {
addTaskToTimer(id)
} else {
service.Cron.RemoveJob(strconv.Itoa(id))
}
return json.Success(utils.SuccessContent, nil)
return json.Success(utils.SuccessContent, nil)
}
// 添加任务到定时器
func addTaskToTimer(id int) {
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil {
logger.Error(err)
return
}
func addTaskToTimer(id int) {
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil {
logger.Error(err)
return
}
taskService := service.Task{}
taskService.Add(task)
taskService := service.Task{}
taskService.Add(task)
}
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
var params models.CommonMap = models.CommonMap{}
params["Id"] = ctx.QueryInt("id")
params["HostId"] = ctx.QueryInt("host_id")
params["Name"] = ctx.QueryTrim("name")
params["Protocol"] = ctx.QueryInt("protocol")
status := ctx.QueryInt("status")
if status >=0 {
status -= 1
}
params["Status"] = status
base.ParsePageAndPageSize(ctx, params)
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
var params models.CommonMap = models.CommonMap{}
params["Id"] = ctx.QueryInt("id")
params["HostId"] = ctx.QueryInt("host_id")
params["Name"] = ctx.QueryTrim("name")
params["Protocol"] = ctx.QueryInt("protocol")
params["Tag"] = ctx.QueryTrim("tag")
status := ctx.QueryInt("status")
if status >= 0 {
status -= 1
}
params["Status"] = status
base.ParsePageAndPageSize(ctx, params)
return params
return params
}
func setHostsToTemplate(ctx *macaron.Context) {
hostModel := new(models.Host)
hosts, err := hostModel.List(models.CommonMap{})
if err != nil {
logger.Error(err)
}
ctx.Data["Hosts"] = hosts
}
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
}
+60 -107
View File
@@ -3,125 +3,78 @@ package tasklog
// 任务日志
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/Unknwon/paginater"
"fmt"
"html/template"
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"errors"
"fmt"
"github.com/Unknwon/paginater"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/base"
"gopkg.in/macaron.v1"
"html/template"
)
func Index(ctx *macaron.Context) {
logModel := new(models.TaskLog)
queryParams := parseQueryParams(ctx)
total, err := logModel.Total(queryParams)
if err != nil {
logger.Error(err)
}
logs, err := logModel.List(queryParams)
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("task_id=%d&protocol=%d&status=%d&page_size=%d",
queryParams["TaskId"], queryParams["Protocol"], 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["Logs"] = logs
ctx.Data["Params"] = queryParams
ctx.HTML(200, "task/log")
func Index(ctx *macaron.Context) {
logModel := new(models.TaskLog)
queryParams := parseQueryParams(ctx)
total, err := logModel.Total(queryParams)
if err != nil {
logger.Error(err)
}
logs, err := logModel.List(queryParams)
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("task_id=%d&protocol=%d&status=%d&page_size=%d",
queryParams["TaskId"], queryParams["Protocol"], 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["Logs"] = logs
ctx.Data["Params"] = queryParams
ctx.HTML(200, "task/log")
}
// 清空日志
func Clear(ctx *macaron.Context) string {
taskLogModel := new(models.TaskLog)
_, err := taskLogModel.Clear()
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure(utils.FailureContent)
}
func Clear(ctx *macaron.Context) string {
taskLogModel := new(models.TaskLog)
_, err := taskLogModel.Clear()
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure(utils.FailureContent)
}
return json.Success(utils.SuccessContent, nil)
return json.Success(utils.SuccessContent, nil)
}
// 删除N个月前的日志
func Remove(ctx *macaron.Context) string {
month := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
if month < 1 || month > 12 {
return json.CommonFailure("参数取值范围1-12")
}
taskLogModel := new(models.TaskLog)
_, err := taskLogModel.Remove(month)
if err != nil {
return json.CommonFailure("删除失败", err)
}
month := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
if month < 1 || month > 12 {
return json.CommonFailure("参数取值范围1-12")
}
taskLogModel := new(models.TaskLog)
_, err := taskLogModel.Remove(month)
if err != nil {
return json.CommonFailure("删除失败", err)
}
return json.Success("删除成功", nil)
}
// 更新任务状态
func UpdateStatus(ctx *macaron.Context) string {
id := ctx.QueryTrim("id")
status := ctx.QueryInt("status")
result := ctx.QueryTrim("result")
json := utils.JsonResponse{}
if id == "" {
return json.CommonFailure("任务ID不能为空")
}
if status != 1 && status != 2 {
return json.CommonFailure("status值错误")
}
if status == 1 {
status -= 1
}
taskLogModel := new(models.TaskLog)
affectRows, err := taskLogModel.UpdateStatus(id, models.Status(status), result)
if err != nil || affectRows == 0 {
return json.CommonFailure("更新任务状态失败")
}
// 发送通知
taskId, err := taskLogModel.GetTaskIdByNotifyId(id)
if err != nil || taskId <= 0 {
logger.Error("异步任务回调#根据notify-id获取taskId失败", err)
return json.Success("success", nil)
}
taskModel := new(models.Task)
task, err := taskModel.Detail(taskId)
if err != nil || task.Id <= 0 {
logger.Error("异步任务回调#根据获取任务详情失败", err)
return json.Success("success", nil)
}
taskResult := service.TaskResult{}
taskResult.Result = result
if status == 0 {
taskResult.Err = errors.New("error")
}
service.SendNotification(task, taskResult)
return json.Success("success", nil)
return json.Success("删除成功", nil)
}
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
var params models.CommonMap = models.CommonMap{}
params["TaskId"] = ctx.QueryInt("task_id")
params["Protocol"] = ctx.QueryInt("protocol")
status := ctx.QueryInt("status")
if status >=0 {
status -= 1
}
params["Status"] = status
base.ParsePageAndPageSize(ctx, params)
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
var params models.CommonMap = models.CommonMap{}
params["TaskId"] = ctx.QueryInt("task_id")
params["Protocol"] = ctx.QueryInt("protocol")
status := ctx.QueryInt("status")
if status >= 0 {
status -= 1
}
params["Status"] = status
base.ParsePageAndPageSize(ctx, params)
return params
}
return params
}
+300 -92
View File
@@ -1,123 +1,331 @@
package user
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/models"
"github.com/go-macaron/session"
"github.com/ouqiang/gocron/modules/logger"
"github.com/go-macaron/captcha"
"github.com/go-macaron/session"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/base"
"gopkg.in/macaron.v1"
"github.com/Unknwon/paginater"
"html/template"
"fmt"
"strings"
)
// @author qiang.ou<qingqianludao@gmail.com>
// @date 2017/4/23-14:16
func Login(ctx *macaron.Context) {
ctx.Data["Title"] = "用户登录"
ctx.HTML(200, "user/login")
// UserForm 用户表单
type UserForm struct {
Id int
Name string `binding:"Required;MaxSize(32)"` // 用户名
Password string // 密码
ConfirmPassword string // 确认密码
Email string `binding:"Required;MaxSize(50)"` // 邮箱
IsAdmin int8 // 是否是管理员 1:管理员 0:普通用户
Status models.Status
}
func EditPassword(ctx *macaron.Context) {
ctx.Data["Title"] = "修改密码"
ctx.HTML(200, "user/editPassword")
// Index 用户列表页
func Index(ctx *macaron.Context) {
queryParams := parseQueryParams(ctx)
userModel := new(models.User)
users, err := userModel.List(queryParams)
if err != nil {
logger.Error(err)
}
total, err := userModel.Total()
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("page_size=%d", 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["Params"] = queryParams
ctx.Data["Title"] = "用户列表"
ctx.Data["Users"] = users
ctx.HTML(200, "user/index")
}
func UpdatePassword(ctx *macaron.Context, sess session.Store) string {
oldPassword := ctx.QueryTrim("old_password")
newPassword := ctx.QueryTrim("new_password")
confirmNewPassword := ctx.QueryTrim("confirm_new_password")
json := utils.JsonResponse{}
if oldPassword == "" || newPassword == "" || confirmNewPassword == "" {
return json.CommonFailure("原密码和新密码均不能为空")
}
if newPassword != confirmNewPassword {
return json.CommonFailure("两次输入密码不一致")
}
if oldPassword == newPassword {
return json.CommonFailure("原密码与新密码不能相同")
}
userModel := new(models.User)
if !userModel.Match(Username(sess), oldPassword) {
return json.CommonFailure("原密码输入错误")
}
_, err := userModel.UpdatePassword(Uid(sess), newPassword)
if err != nil {
return json.CommonFailure("修改失败")
}
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
var params models.CommonMap = models.CommonMap{}
base.ParsePageAndPageSize(ctx, params)
return json.Success("修改成功", nil)
return params
}
func ValidateLogin(ctx *macaron.Context, sess session.Store) string {
username := ctx.QueryTrim("username")
password := ctx.QueryTrim("password")
json := utils.JsonResponse{}
if username == "" || password == "" {
return json.CommonFailure("用户名、密码不能为空")
}
userModel := new (models.User)
if !userModel.Match(username, password) {
return json.CommonFailure("用户名或密码错误")
}
loginLogModel := new(models.LoginLog)
loginLogModel.Username = userModel.Name
loginLogModel.Ip = ctx.RemoteAddr()
_, err := loginLogModel.Create()
if err != nil {
logger.Error("记录用户登录日志失败", err)
}
sess.Set("username", userModel.Name)
sess.Set("uid", userModel.Id)
sess.Set("isAdmin", userModel.IsAdmin)
return json.Success("登录成功", nil)
// Create 新增用户页
func Create(ctx *macaron.Context) {
userModel := new(models.User)
userModel.Status = models.Enabled
userModel.IsAdmin = 0
ctx.Data["User"] = userModel
ctx.Data["Title"] = "添加用户"
ctx.HTML(200, "user/user_form")
}
// 编辑页面
func Edit(ctx *macaron.Context) {
ctx.Data["Title"] = "编辑用户"
userModel := new(models.User)
id := ctx.ParamsInt(":id")
err := userModel.Find(id)
if err != nil {
logger.Error(err)
}
ctx.Data["User"] = userModel
ctx.HTML(200, "user/user_form")
}
// 保存任务
func Store(ctx *macaron.Context, form UserForm) string {
form.Name = strings.TrimSpace(form.Name)
form.Email = strings.TrimSpace(form.Email)
form.Password = strings.TrimSpace(form.Password)
form.ConfirmPassword = strings.TrimSpace(form.ConfirmPassword)
json := utils.JsonResponse{}
userModel := models.User{}
nameExists, err := userModel.UsernameExists(form.Name, form.Id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if nameExists > 0 {
return json.CommonFailure("用户名已存在")
}
emailExists, err := userModel.EmailExists(form.Email, form.Id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if emailExists > 0 {
return json.CommonFailure("邮箱已存在")
}
if form.Id == 0 {
if form.Password == "" {
return json.CommonFailure("请输入密码")
}
if form.ConfirmPassword == "" {
return json.CommonFailure("请再次输入密码")
}
if form.Password != form.ConfirmPassword {
return json.CommonFailure("两次密码输入不一致")
}
}
userModel.Name = form.Name
userModel.Email = form.Email
userModel.Password = form.Password
userModel.IsAdmin = form.IsAdmin
userModel.Status = form.Status
if form.Id == 0 {
_, err = userModel.Create()
if err != nil {
return json.CommonFailure("添加失败", err)
}
} else {
_, err = userModel.Update(form.Id, models.CommonMap{
"name": form.Name,
"email": form.Email,
"status": form.Status,
"is_admin": form.IsAdmin,
})
if err != nil {
return json.CommonFailure("修改失败", err)
}
}
return json.Success("保存成功", nil)
}
// 删除用户
func Remove(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
userModel := new(models.User)
_, err := userModel.Delete(id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
return json.Success(utils.SuccessContent, nil)
}
// 激活用户
func Enable(ctx *macaron.Context) string {
return changeStatus(ctx, models.Enabled)
}
// 禁用用户
func Disable(ctx *macaron.Context) string {
return changeStatus(ctx, models.Disabled)
}
// 改变任务状态
func changeStatus(ctx *macaron.Context, status models.Status) string {
id := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
userModel := new(models.User)
_, err := userModel.Update(id, models.CommonMap{
"Status": status,
})
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
return json.Success(utils.SuccessContent, nil)
}
// Login 用户登录
func Login(ctx *macaron.Context) {
ctx.Data["Title"] = "用户登录"
ctx.HTML(200, "user/login")
}
// EditPassword 修改密码页面
func EditPassword(ctx *macaron.Context) {
id := ctx.ParamsInt(":id")
ctx.Data["Title"] = "修改密码"
ctx.Data["Id"] = id
ctx.HTML(200, "user/editPassword")
}
// UpdatePassword 更新我的密码
func UpdatePassword(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
newPassword := ctx.QueryTrim("new_password")
confirmNewPassword := ctx.QueryTrim("confirm_new_password")
json := utils.JsonResponse{}
if newPassword == "" || confirmNewPassword == "" {
return json.CommonFailure("请输入密码")
}
userModel := new(models.User)
_, err := userModel.UpdatePassword(id, newPassword)
if err != nil {
return json.CommonFailure("修改失败")
}
return json.Success("修改成功", nil)
}
// EditMyPassword 修改我的密码页面
func EditMyPassword(ctx *macaron.Context) {
ctx.Data["Title"] = "修改密码"
ctx.HTML(200, "user/editMyPassword")
}
// UpdateMyPassword 更新我的密码
func UpdateMyPassword(ctx *macaron.Context, sess session.Store) string {
oldPassword := ctx.QueryTrim("old_password")
newPassword := ctx.QueryTrim("new_password")
confirmNewPassword := ctx.QueryTrim("confirm_new_password")
json := utils.JsonResponse{}
if oldPassword == "" || newPassword == "" || confirmNewPassword == "" {
return json.CommonFailure("原密码和新密码均不能为空")
}
if newPassword != confirmNewPassword {
return json.CommonFailure("两次输入密码不一致")
}
if oldPassword == newPassword {
return json.CommonFailure("原密码与新密码不能相同")
}
userModel := new(models.User)
if !userModel.Match(Username(sess), oldPassword) {
return json.CommonFailure("原密码输入错误")
}
_, err := userModel.UpdatePassword(Uid(sess), newPassword)
if err != nil {
return json.CommonFailure("修改失败")
}
return json.Success("修改成功", nil)
}
// ValidateLogin 验证用户登录
func ValidateLogin(ctx *macaron.Context, sess session.Store, cpt *captcha.Captcha) string {
username := ctx.QueryTrim("username")
password := ctx.QueryTrim("password")
json := utils.JsonResponse{}
if username == "" || password == "" {
return json.CommonFailure("用户名、密码不能为空")
}
userModel := new(models.User)
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
loginLogModel.Ip = ctx.RemoteAddr()
_, err := loginLogModel.Create()
if err != nil {
logger.Error("记录用户登录日志失败", err)
}
sess.Set("username", userModel.Name)
sess.Set("uid", userModel.Id)
sess.Set("isAdmin", userModel.IsAdmin)
return json.Success("登录成功", nil)
}
// Logout 用户退出
func Logout(ctx *macaron.Context, sess session.Store) {
if IsLogin(sess) {
err := sess.Destory(ctx)
if err != nil {
logger.Error("用户退出登录失败", err)
}
}
if IsLogin(sess) {
err := sess.Destory(ctx)
if err != nil {
logger.Error("用户退出登录失败", err)
}
}
Login(ctx)
Login(ctx)
}
func Username(sess session.Store) string {
username,ok := sess.Get("username").(string)
if ok {
return username
}
// Username 获取session中的用户名
func Username(sess session.Store) string {
username, ok := sess.Get("username").(string)
if ok {
return username
}
return ""
return ""
}
func Uid(sess session.Store) int {
uid,ok := sess.Get("uid").(int)
if ok {
return uid
}
// Uid 获取session中的Uid
func Uid(sess session.Store) int {
uid, ok := sess.Get("uid").(int)
if ok {
return uid
}
return 0
return 0
}
func IsLogin(sess session.Store) bool {
uid, ok := sess.Get("uid").(int)
if ok && uid > 0 {
return true
}
// IsLogin 判断用户是否已登录
func IsLogin(sess session.Store) bool {
uid, ok := sess.Get("uid").(int)
if ok && uid > 0 {
return true
}
return false
return false
}
func IsAdmin(sess session.Store) bool {
isAdmin, ok := sess.Get("isAdmin").(int8)
if ok && isAdmin > 0 {
return true
}
// IsAdmin 判断当前用户是否是管理员
func IsAdmin(sess session.Store) bool {
isAdmin, ok := sess.Get("isAdmin").(int8)
if ok && isAdmin > 0 {
return true
}
return false
}
return false
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 295 KiB

-110
View File
@@ -1,110 +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"
)
var tw *timewheel.TimeWheel
type DelayTask struct {}
// 从数据库中取出所有延迟任务
func (task *DelayTask) Initialize(tick time.Duration, slots int) {
tw = timewheel.New(tick, slots)
tw.Start()
taskModel := new(models.DelayTask)
currentTime := time.Now()
taskNum, err := taskModel.ActiveNum(currentTime)
if err != nil {
logger.Error("延迟任务初始化#获取待执行的任务失败", err)
return
}
if taskNum == 0 {
logger.Debugf("延迟任务初始化#待执行的任务数量为0")
return
}
pageSize := 100
totalPage := int( math.Ceil(float64(taskNum) / float64(pageSize)) )
logger.Infof("延迟任务初始化#待执行的任务数量-%d#共%d页#每页取%d条", taskNum, totalPage, pageSize)
taskModel.PageSize = pageSize
for page := 1; page <= totalPage; page++ {
taskModel.Page = page
logger.Debugf("延迟任务初始化#取出任务列表#第%d页", page)
taskList, err := taskModel.ActiveList(currentTime)
if err != nil {
logger.Error("延迟任务初始化#获取任务列表失败", err)
}
task.BatchAdd(taskList)
}
logger.Info("延迟任务初始化完成")
}
// 批量添加任务
func (task *DelayTask) BatchAdd(taskList []models.DelayTask) {
for _, item := range(taskList) {
task.Add(item)
}
}
// 添加任务
func (task *DelayTask) Add(taskModel models.DelayTask) {
currentTimestamp := time.Now().Unix()
execTimestamp := taskModel.Created.Unix() + int64(taskModel.Delay)
// 时间过期, 立即执行任务
if execTimestamp <= currentTimestamp {
go task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
return
}
delay := execTimestamp - currentTimestamp
tw.Add(time.Duration(delay) * time.Second, func() {
task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
})
}
// 运行任务
func (task *DelayTask) Run(id int64, url, params string) {
taskModel := new(models.DelayTask)
_, err := taskModel.UpdateStatus(id, models.Running)
if err != nil {
logger.Error("延迟任务开始执行#更新任务状态失败", err)
return
}
timeout := 300
tryTimes := 3
success := false
logger.Infof("延迟任务开始执行#id-%d#url-%s#params-%s", id, url, params)
for i := 0; i < tryTimes; {
response := httpclient.PostParams(url, params, timeout)
if response.StatusCode == 200 && strings.TrimSpace(response.Body) == "success"{
success = true
break;
}
i++
if i < tryTimes {
logger.Errorf("延迟任务执行失败#重试第%d次#任务Id-%d#HTTP状态码-%d#HTTP-BODY-%s",
i,id,response.StatusCode,response.Body)
time.Sleep(30 * time.Second)
}
}
logger.Infof("延迟任务执行完成#id-%d", id)
var status models.Status
if success {
status = models.Finish
} else {
status = models.Failure
}
_ ,err = taskModel.UpdateStatus(id, status)
if err != nil {
logger.Error("延迟任务执行完成#更新任务状态失败", err)
}
}
func (task *DelayTask) Stop() {
tw.Stop()
}
+312 -309
View File
@@ -1,173 +1,153 @@
package service
import (
"github.com/ouqiang/gocron/models"
"strconv"
"time"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/jakecoffman/cron"
"github.com/ouqiang/gocron/modules/utils"
"errors"
"fmt"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/notify"
"sync"
"errors"
"fmt"
"github.com/jakecoffman/cron"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/notify"
rpcClient "github.com/ouqiang/gocron/modules/rpc/client"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"strconv"
"strings"
"sync"
"time"
)
// 定时任务调度管理器
var Cron *cron.Cron
// 同一任务是否有实例处于运行中
var runInstance Instance
// 任务计数-正在运行中的任务
var TaskNum TaskCount
// 任务计数
type TaskCount struct {
num int
sync.RWMutex
num int
sync.RWMutex
}
func (c *TaskCount) Add() {
c.Lock()
defer c.Unlock()
c.num += 1
func (c *TaskCount) Add() {
c.Lock()
defer c.Unlock()
c.num += 1
}
func (c *TaskCount) Done() {
c.Lock()
defer c.Unlock()
c.num -= 1
func (c *TaskCount) Done() {
c.Lock()
defer c.Unlock()
c.num -= 1
}
func (c *TaskCount) Num() int {
c.RLock()
defer c.RUnlock()
func (c *TaskCount) Num() int {
c.RLock()
defer c.RUnlock()
return c.num
return c.num
}
// 任务ID作为Key, 不会出现并发写, 不加锁
// 任务ID作为Key
type Instance struct {
Status map[int]bool
Status map[int]bool
sync.RWMutex
}
// 是否有任务处于运行中
func (i *Instance) has(key int) bool {
running, ok := i.Status[key]
if ok && running {
return true
}
i.RLock()
defer i.RUnlock()
running, ok := i.Status[key]
if ok && running {
return true
}
return false
return false
}
func (i *Instance) add(key int) {
i.Status[key] = true
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
func (i *Instance) done(key int) {
i.Lock()
defer i.Unlock()
delete(i.Status, key)
}
type Task struct{}
type TaskResult struct {
Result string
Err error
RetryTimes int8
IsAsync bool
Result string
Err error
RetryTimes int8
}
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
func (task *Task) Initialize() {
Cron = cron.New()
Cron.Start()
runInstance = Instance{make(map[int]bool)}
TaskNum = TaskCount{0, sync.RWMutex{}}
Cron = cron.New()
Cron.Start()
runInstance = Instance{make(map[int]bool), sync.RWMutex{}}
TaskNum = TaskCount{0, sync.RWMutex{}}
taskModel := new(models.Task)
taskList, err := taskModel.ActiveList()
if err != nil {
logger.Error("定时任务初始化#获取任务列表错误-", err.Error())
return
}
if len(taskList) == 0 {
logger.Debug("任务列表为空")
return
}
task.BatchAdd(taskList)
taskModel := new(models.Task)
taskList, err := taskModel.ActiveList()
if err != nil {
logger.Error("定时任务初始化#获取任务列表错误-", err.Error())
return
}
if len(taskList) == 0 {
logger.Debug("任务列表为空")
return
}
task.BatchAdd(taskList)
}
// 批量添加任务
func (task *Task) BatchAdd(tasks []models.TaskHost) {
for _, item := range tasks {
task.Add(item)
}
func (task *Task) BatchAdd(tasks []models.Task) {
for _, item := range tasks {
task.Add(item)
}
}
// 添加任务
func (task *Task) Add(taskModel models.TaskHost) {
taskFunc := createJob(taskModel)
if taskFunc == nil {
logger.Error("创建任务处理Job失败,不支持的任务协议#", taskModel.Protocol)
return
}
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)
return
}
cronName := strconv.Itoa(taskModel.Id)
// Cron任务采用数组存储, 删除任务需遍历数组, 并对数组重新赋值, 任务较多时,有性能问题
Cron.RemoveJob(cronName)
err := Cron.AddFunc(taskModel.Spec, taskFunc, cronName)
if err != nil {
logger.Error("添加任务到调度器失败#", err)
}
cronName := strconv.Itoa(taskModel.Id)
// Cron任务采用数组存储, 删除任务需遍历数组, 并对数组重新赋值, 任务较多时,有性能问题
Cron.RemoveJob(cronName)
err := Cron.AddFunc(taskModel.Spec, taskFunc, cronName)
if err != nil {
logger.Error("添加任务到调度器失败#", err)
}
}
// 停止所有任务
func (task *Task) StopAll() {
Cron.Stop()
func (task *Task) StopAll() {
Cron.Stop()
}
// 直接运行任务
func (task *Task) Run(taskModel models.TaskHost) {
go createJob(taskModel)()
func (task *Task) Run(taskModel models.Task) {
go createJob(taskModel)()
}
type Handler interface {
Run(taskModel models.TaskHost) (string, error)
}
// 本地命令
type LocalCommandHandler struct {}
// 运行本地命令
func (h *LocalCommandHandler) Run(taskModel models.TaskHost) (string, error) {
if taskModel.Command == "" {
return "", errors.New("invalid command")
}
if utils.IsWindows() {
return h.runOnWindows(taskModel)
}
return h.runOnUnix(taskModel)
}
// 执行Windows命令
func (h *LocalCommandHandler) runOnWindows(taskModel models.TaskHost) (string, error) {
outputGBK, err := utils.ExecShellWithTimeout(taskModel.Timeout, "cmd", "/C", taskModel.Command)
// windows平台编码为gbk,需转换为utf8才能入库
outputUTF8, ok := utils.GBK2UTF8(outputGBK)
if ok {
return outputUTF8, err
}
return "命令输出转换编码失败(gbk to utf8)", err
}
// 执行Unix命令
func (h *LocalCommandHandler) runOnUnix(taskModel models.TaskHost) (string, error) {
return utils.ExecShellWithTimeout(taskModel.Timeout, "/bin/bash", "-c", taskModel.Command)
Run(taskModel models.Task) (string, error)
}
// HTTP任务
@@ -176,235 +156,258 @@ type HTTPHandler struct{}
// http任务执行时间不超过300秒
const HttpExecTimeout = 300
func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout
}
resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
// 返回状态码非200,均为失败
if resp.StatusCode != 200 {
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
}
func (h *HTTPHandler) Run(taskModel models.Task) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout
}
resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
// 返回状态码非200,均为失败
if resp.StatusCode != 200 {
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
}
return resp.Body, err
return resp.Body, err
}
// SSH-command任务
type SSHCommandHandler struct{}
// RPC调用执行任务
type RPCHandler struct{}
func (h *SSHCommandHandler) Run(taskModel models.TaskHost) (string, error) {
hostModel := new(models.Host)
err := hostModel.Find(int(taskModel.HostId))
if err != nil {
return "", err
}
sshConfig := ssh.SSHConfig{}
sshConfig.User = hostModel.Username
sshConfig.Host = hostModel.Name
sshConfig.Port = hostModel.Port
sshConfig.ExecTimeout = taskModel.Timeout
sshConfig.AuthType = hostModel.AuthType
var password string
var privateKey string
if hostModel.AuthType == ssh.HostPassword {
password, err = hostModel.GetPasswordByHost(hostModel.Name)
if err != nil {
return "", err
}
sshConfig.Password = password
} else {
privateKey, err = hostModel.GetPrivateKeyByHost(hostModel.Name)
if err != nil {
return "", err
}
sshConfig.PrivateKey = privateKey
}
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.Exec(th.Name, th.Port, 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 ssh.Exec(sshConfig, taskModel.Command)
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, string, error) {
taskLogModel := new(models.TaskLog)
taskLogModel.TaskId = taskModel.Id
taskLogModel.Name = taskModel.Task.Name
taskLogModel.Spec = taskModel.Spec
taskLogModel.Protocol = taskModel.Protocol
taskLogModel.Command = taskModel.Command
taskLogModel.Timeout = taskModel.Timeout
if taskModel.Protocol == models.TaskSSH {
taskLogModel.Hostname = taskModel.Alias + "-" + taskModel.Name
}
taskLogModel.StartTime = time.Now()
taskLogModel.Status = status
// SSH执行远程命令,后台运行
var notifyId string = ""
if taskModel.Timeout == -1 {
notifyId = utils.RandString(32);
taskLogModel.NotifyId = notifyId;
}
insertId, err := taskLogModel.Create()
func createTaskLog(taskModel models.Task, status models.Status) (int64, error) {
taskLogModel := new(models.TaskLog)
taskLogModel.TaskId = taskModel.Id
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 {
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
insertId, err := taskLogModel.Create()
return insertId, notifyId, err
return insertId, err
}
// 更新任务日志
func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) {
taskLogModel := new(models.TaskLog)
var status models.Status
var result string = taskResult.Result
if taskResult.Err != nil {
status = models.Failure
} else if taskResult.IsAsync {
status = models.Background
} else {
status = models.Finish
}
return taskLogModel.Update(taskLogId, models.CommonMap{
"retry_times": taskResult.RetryTimes,
"status": status,
"result": result,
})
taskLogModel := new(models.TaskLog)
var status models.Status
var result string = taskResult.Result
if taskResult.Err != nil {
status = models.Failure
} else {
status = models.Finish
}
return taskLogModel.Update(taskLogId, models.CommonMap{
"retry_times": taskResult.RetryTimes,
"status": status,
"result": result,
})
}
func createJob(taskModel models.TaskHost) cron.FuncJob {
var handler Handler = createHandler(taskModel)
if handler == nil {
return nil
}
taskFunc := func() {
TaskNum.Add()
defer TaskNum.Done()
taskLogId := beforeExecJob(&taskModel)
if taskLogId <= 0 {
return
}
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
taskResult := execJob(handler, taskModel)
logger.Infof("任务完成#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId)
}
func createJob(taskModel models.Task) cron.FuncJob {
var handler Handler = createHandler(taskModel)
if handler == nil {
return nil
}
taskFunc := func() {
TaskNum.Add()
defer TaskNum.Done()
taskLogId := beforeExecJob(taskModel)
if taskLogId <= 0 {
return
}
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command)
taskResult := execJob(handler, taskModel)
logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId)
}
return taskFunc
return taskFunc
}
func createHandler(taskModel models.TaskHost) Handler {
var handler Handler = nil
switch taskModel.Protocol {
case models.TaskHTTP:
handler = new(HTTPHandler)
case models.TaskSSH:
handler = new(SSHCommandHandler)
case models.TaskLocalCommand:
handler = new(LocalCommandHandler)
}
func createHandler(taskModel models.Task) Handler {
var handler Handler = nil
switch taskModel.Protocol {
case models.TaskHTTP:
handler = new(HTTPHandler)
case models.TaskRPC:
handler = new(RPCHandler)
}
return handler;
return handler
}
func beforeExecJob(taskModel *models.TaskHost) (taskLogId int64) {
if taskModel.Multi == 0 && runInstance.has(taskModel.Id) {
createTaskLog(*taskModel, models.Cancel)
return
}
if taskModel.Multi == 0 {
runInstance.add(taskModel.Id)
}
taskLogId, notifyId, err := createTaskLog(*taskModel, models.Running)
if err != nil {
logger.Error("任务开始执行#写入任务日志失败-", err)
return
}
// 设置notifyId到环境变量中
if notifyId != "" {
envName := "GOCRON_TASK_ID"
if taskModel.Protocol == models.TaskSSH {
taskModel.Command = fmt.Sprintf("%s%s", utils.FormatUnixEnv(envName, notifyId), taskModel.Command)
} else {
taskModel.Command = fmt.Sprintf("%s%s", utils.FormatEnv(envName, notifyId), taskModel.Command)
}
}
// 任务前置操作
func beforeExecJob(taskModel models.Task) (taskLogId int64) {
if taskModel.Multi == 0 && runInstance.has(taskModel.Id) {
createTaskLog(taskModel, models.Cancel)
return
}
if taskModel.Multi == 0 {
runInstance.add(taskModel.Id)
}
taskLogId, err := createTaskLog(taskModel, models.Running)
if err != nil {
logger.Error("任务开始执行#写入任务日志失败-", err)
return
}
logger.Debugf("任务命令-%s", taskModel.Command)
logger.Debugf("任务命令-%s", taskModel.Command)
return taskLogId
return taskLogId
}
func afterExecJob(taskModel models.TaskHost, taskResult TaskResult, taskLogId int64) {
if taskResult.Err != nil {
taskResult.Result = taskResult.Err.Error() + "\n" + taskResult.Result
}
if taskModel.Timeout == -1 {
taskResult.IsAsync = true
}
_, err := updateTaskLog(taskLogId, taskResult)
if err != nil {
logger.Error("任务结束#更新任务日志失败-", err)
}
if taskResult.IsAsync {
return
}
// 任务执行后置操作
func afterExecJob(taskModel models.Task, taskResult TaskResult, taskLogId int64) {
_, err := updateTaskLog(taskLogId, taskResult)
if err != nil {
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.Spec = fmt.Sprintf("依赖任务(主任务ID-%d)", taskModel.Id)
serviceTask.Run(task)
}
}
// 发送任务结果通知
func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
var statusName string
// 未开启通知
if taskModel.NotifyStatus == 0 {
return
}
if taskModel.NotifyStatus == 1 && taskResult.Err == nil {
// 执行失败才发送通知
return
}
if taskModel.NotifyReceiverId == "" {
return
}
if taskResult.Err != nil {
statusName = "失败"
} else {
statusName = "成功"
}
// 发送通知
msg := notify.Message{
"task_type": taskModel.NotifyType,
"task_receiver_id": taskModel.NotifyReceiverId,
"name": taskModel.Task.Name,
"output": taskResult.Result,
"status": statusName,
"taskId": taskModel.Id,
};
notify.Push(msg)
func SendNotification(taskModel models.Task, taskResult TaskResult) {
var statusName string
// 未开启通知
if taskModel.NotifyStatus == 0 {
return
}
if taskModel.NotifyStatus == 1 && taskResult.Err == nil {
// 执行失败才发送通知
return
}
if taskModel.NotifyReceiverId == "" {
return
}
if taskResult.Err != nil {
statusName = "失败"
} else {
statusName = "成功"
}
// 发送通知
msg := notify.Message{
"task_type": taskModel.NotifyType,
"task_receiver_id": taskModel.NotifyReceiverId,
"name": taskModel.Name,
"output": taskResult.Result,
"status": statusName,
"taskId": taskModel.Id,
}
notify.Push(msg)
}
// 执行具体任务
func execJob(handler Handler, taskModel models.TaskHost) TaskResult {
if taskModel.Multi == 0 {
defer runInstance.done(taskModel.Id)
}
// 默认只运行任务一次
var execTimes int8 = 1
if (taskModel.RetryTimes > 0) {
execTimes += taskModel.RetryTimes
}
var i int8 = 0
var output string
var err error
for i < execTimes {
output, err = handler.Run(taskModel)
if err == nil {
return TaskResult{Result: output, Err: err, RetryTimes: i}
}
i++
if i < execTimes {
logger.Warnf("任务执行失败#任务id-%d#重试第%d次#输出-%s#错误-%s", taskModel.Id, i, output, err.Error())
// 重试间隔时间,每次递增1分钟
time.Sleep( time.Duration(i) * time.Minute)
}
}
func execJob(handler Handler, taskModel models.Task) TaskResult {
defer func() {
if err := recover(); err != nil {
logger.Error("panic#service/task.go:execJob#", err)
}
}()
if taskModel.Multi == 0 {
defer runInstance.done(taskModel.Id)
}
// 默认只运行任务一次
var execTimes int8 = 1
if taskModel.RetryTimes > 0 {
execTimes += taskModel.RetryTimes
}
var i int8 = 0
var output string
var err error
for i < execTimes {
output, err = handler.Run(taskModel)
if err == nil {
return TaskResult{Result: output, Err: err, RetryTimes: i}
}
i++
if i < execTimes {
logger.Warnf("任务执行失败#任务id-%d#重试第%d次#输出-%s#错误-%s", taskModel.Id, i, output, err.Error())
// 重试间隔时间,每次递增1分钟
time.Sleep(time.Duration(i) * time.Minute)
}
}
return TaskResult{Result: output, Err: err, RetryTimes: taskModel.RetryTimes}
}
return TaskResult{Result: output, Err: err, RetryTimes: taskModel.RetryTimes}
}
+6 -7
View File
@@ -48,7 +48,7 @@
你好{{{.LoginUsername}}}
<i class="dropdown icon"></i>
<div class="menu">
<a href="/user/editPassword" class="item"><i class="edit icon"></i>修改密码</a>
<a href="/user/editMyPassword" class="item"><i class="edit icon"></i>修改密码</a>
<a class="item" href="/user/logout"><i class="sign out icon"></i>退出</a>
</div>
{{{end}}}
@@ -60,13 +60,12 @@
<div class="ui teal inverted menu">
<div class="bigcontainer">
<div class="right menu">
<a class="item {{{if eq .Controller "task"}}}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 "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>
<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>
{{{if .IsAdmin}}}
<a class="item {{{if eq .Controller "user"}}}active{{{end}}}" href="/user"><i class="user icon"></i>用户管理</a>
<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>
+8 -43
View File
@@ -17,46 +17,24 @@
<input type="hidden" name="id" value="{{{.Host.Id}}}">
<div class="four fields">
<div class="field">
<label>主机名 (域名或IP)</label>
<label>主机名</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.Host.Name}}}">
<input type="text" name="name" value="{{{.Host.Name}}}" placeholder="127.0.0.1">
</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="port" value="{{{.Host.Port}}}" placeholder="5921">
</div>
</div>
</div>
<div class="four fields">
<div class="field">
<label>SSH用户名</label>
<label>节点名称</label>
<div class="ui small input">
<input type="text" name="username" value="{{{.Host.Username}}}">
<input type="text" name="alias" value="{{{.Host.Alias}}}"
placeholder="节点名称如web">
</div>
</div>
<div class="field">
<label>SSH端口</label>
<div class="ui small input">
<input type="text" name="port" value="{{{.Host.Port}}}">
</div>
</div>
</div>
<div class="fields">
<div class="field">
<div class="ui blue message">
密码认证: 把密码写入文件 gocron根目录/data/ssh/password/主机名<br>
: echo '12345678' > data/ssh/password/127.0.0.1<br><br>
公钥认证: 把私钥写入文件 gocron根目录/data/ssh/private_key/主机名<br>
: cp ~/.ssh/id_rsa data/ssh/private_key/127.0.0.1
</div>
<label>认证方式</label>
<select name="auth_type" id="authType">
<option value="2" {{{if .Host}}} {{{if eq .Host.AuthType 2}}}selected {{{end}}} {{{end}}} data-validate-type="selectPrivateKey" data-match="private_key">公钥</option>
<option value="1" {{{if .Host}}} {{{if eq .Host.AuthType 1}}}selected {{{end}}} {{{end}}} data-validate-type="selectPassword" data-match="password">密码</option>
</select>
</div>
</div>
<div class="two fields">
<div class="field">
@@ -68,7 +46,7 @@
</div>
<div class="ui primary submit button">保存</div>
<a class="ui button" onclick="location.href='/task';">取消</a>
<a class="ui button" onclick="location.href='/host';">取消</a>
</form>
</div>
</div>
@@ -112,19 +90,6 @@
}
]
},
username: {
identifier : 'username',
rules: [
{
type : 'empty',
prompt : '请输入SSH用户名'
},
{
type : 'maxLength[32]',
prompt : '长度不能超过32'
}
]
},
port: {
identifier : 'port',
rules: [
+17 -19
View File
@@ -4,18 +4,20 @@
{{{ template "host/menu" . }}}
<div class="twelve wide column">
<div class="pageHeader">
{{{if .IsAdmin}}}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<a href="/host/create">
<i class="large add icon"></i>
<div class="content">
添加主机
添加节点
</div>
</a>
</h3>
</div>
</div>
{{{end}}}
<form class="ui form">
<div class="three fields">
<div class="field">
@@ -29,13 +31,12 @@
</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>
@@ -47,15 +48,18 @@
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{.Alias}}}</td>
<td>{{{.Username}}}</td>
<td>{{{.Port}}}</td>
<td>{{{.Remark}}}</td>
<td class="operation">
{{{if $.IsAdmin}}}
<a class="ui purple button" href="/host/edit/{{{.Id}}}">编辑</a>
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button><br>
{{{end}}}
<div style="margin-top: 5px;">
<a class="ui twitter button" href="/task?host_id={{{.Id}}}">查看任务</a>
{{{if $.IsAdmin}}}
<button class="ui blue button" @click="ping({{{.Id}}})">连接测试</button>
{{{end}}}
</div>
</td>
</tr>
@@ -68,20 +72,14 @@
<script type="text/javascript">
var Vue = new Vue({
el: '.ui.striped.table',
methods: {
ping: function(id) {
swal({
title: '',
text: "连接中.......",
type: 'info',
showConfirmButton: false
});
util.get("/host/ping/" + id, function(code, message) {
swal('操作成功', '连接成功', 'success');
})
}
}
el: '.ui.celled.table',
methods: {
ping: function(id) {
util.get("/host/ping/" + id, function(code, message) {
swal('操作成功', '连接成功', 'success');
})
}
}
});
</script>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/host"}}}active teal{{{end}}} item" href="/host">
<i class="linux icon"></i> 主机列表
<i class="linux icon"></i> 节点列表
</a>
</div>
</div>
-6
View File
@@ -90,12 +90,6 @@
$('.ui.form').form(
{
onSuccess: function(event, fields) {
swal({
title: '',
text: "系统安装中.......",
type: 'info',
showConfirmButton: false
});
util.post('/install/store', fields, function(code, message) {
swal('安装成功');
setTimeout(function() {
-84
View File
@@ -1,84 +0,0 @@
{{{ template "common/header" . }}}
<style type="text/css">
pre {
white-space: pre-wrap;
word-wrap: break-word;
padding:10px;
background-color: #4C4C4C;
color: white;
}
</style>
<div class="ui grid">
<!--the vertical menu-->
{{{ template "task/menu" . }}}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
</div>
</h3>
</div>
</div>
<form class="ui form">
<div class="fields search">
<div class="field">
<select name="status">
<option value="0">任务状态</option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >失败</option>
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
<option value="6" {{{if eq .Params.Status 5}}}selected{{{end}}}>待执行</option>
</select>
</div>
<div class="field">
<button class="ui linkedin submit button">搜索</button>
</div>
</div>
</form>
<table class="ui pink table">
<thead>
<tr>
<th>任务ID</th>
<th>URL</th>
<th>参数</th>
<th>延迟时间</th>
<th>创建时间</th>
<th>完成时间</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Url}}}</td>
<td>{{{.Params}}}</td>
<td>{{{.Delay}}}</td>
<td>{{{.Created.Format "2006-01-02 15:04:05" }}}</td>
<td>
{{{if or (eq .Status 0) (eq .Status 2) }}}
{{{.Updated.Format "2006-01-02 15:04:05" }}}
{{{end}}}
</td>
<td>
{{{if eq .Status 2}}}
成功
{{{else if eq .Status 1}}}
<span style="color:green">执行中</span>
{{{else if eq .Status 0}}}
<span style="color:red">失败</span>
{{{else if eq .Status 5}}}
<span style="color:#43A102">待执行</span>
{{{end}}}
</td>
</tr>
{{{end}}}
</tbody>
</table>
{{{ template "common/pagination" .}}}
</div>
</div>
{{{ template "common/footer" . }}}
+164 -58
View File
@@ -2,7 +2,8 @@
<div class="ui grid">
{{{template "task/menu" .}}}
<div class="twelve wide column">
<div class="pageHeader">
{{{if .IsAdmin}}}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<a href="/task/create">
@@ -14,6 +15,7 @@
</h3>
</div>
</div>
{{{end}}}
<form class="ui form">
<div class="six fields search">
<div class="one wide field">
@@ -22,9 +24,14 @@
<div class="field">
<input type="text" placeholder="任务名称" name="name" value="{{{.Params.Name}}}">
</div>
<div class="field">
<input type="text" placeholder="标签名称" name="tag" value="{{{.Params.Tag}}}">
</div>
</div>
<div class="six fields search">
<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}}}
@@ -33,15 +40,14 @@
<div class="field">
<select name="protocol" id="protocol">
<option value="0">执行方式</option>
<option value="3" {{{if eq .Params.Protocol 3}}}selected{{{end}}}>系统命令</option>
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SSH</option>
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="1" {{{if eq .Params.Protocol 1}}}selected{{{end}}}>HTTP</option>
</select>
</div>
<div class="field">
<select name="status">
<option value="0">状态</option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} ></option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} ></option>
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>激活</option>
</select>
</div>
@@ -50,70 +56,143 @@
</div>
</div>
</form>
<div class="task-list">
{{{range $i, $v := .Tasks}}}
<div class="ui device two column middle aligned vertical grid segment">
<div class="column verborder">
<div class="ui info segment">
<h5 class="ui header">{{{.Task.Name}}} {{{if eq .Status 1}}}<i class="large checkmark blue icon"></i> {{{else}}} <i class="large red minus icon"></i> {{{end}}}
</h5>
<p>任务ID <span class="stress">{{{.Id}}}</span></p>
<p>状态 <span class="stress">{{{if eq .Status 1}}}激活{{{else}}}停止{{{end}}}</span></p>
<p>cron表达式 {{{.Spec}}}</p>
<p>执行方式: {{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else if eq .Protocol 3}}}本地命令{{{end}}}</p>
<p class="sensorStatus">命令{{{.Command}}}</p>
<p class="sensorStatus">超时时间{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</p>
<p>重试次数: {{{.RetryTimes}}}</p>
<p class="sensorStatus">是否允许多实例运行{{{if gt .Multi 0}}}{{{else}}}{{{end}}}</p>
{{{if eq .Protocol 2}}}
<p>主机: {{{.Alias}}}-{{{.Name}}}</p>
{{{end}}}
<p>备注: {{{.Remark}}}</p>
</div>
</div>
<div class="center aligned column">
<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>
{{{end}}}
<button class="ui positive button" @click="remove({{{.Id}}})">删除</button> <br>
<button class="ui twitter button" @click="run({{{.Id}}})">手动运行</button>
<a class="ui instagram button" href="/task/log?task_id={{{.Id}}}">查看日志</a>
</div>
</div>
</div>
{{{if .IsAdmin}}}
<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>
{{{end}}}
</div>
<br>
<table class="ui celled table task-list">
<thead>
<tr>
{{{if .IsAdmin}}}
<th>
<input type="checkbox" onclick="checkAll(this)" style="width:25px;height: 25px;">
</th>
{{{end}}}
<th>任务ID</th>
<th>任务名称</th>
<th>任务类型</th>
<th>标签</th>
<th>cron表达式</th>
<th>执行方式</th>
<th>超时时间</th>
<th>重试次数</th>
<th>单实例运行</th>
<th>任务节点</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
<tr>
{{{if $.IsAdmin}}}
<td>
<input type="checkbox"
class="sub-check"
data-id="{{{.Id}}}"
style="width:25px;height: 25px;">
</td>
{{{end}}}
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{if eq .Level 1}}}主任务{{{else}}}子任务{{{end}}}</td>
<td>{{{.Tag}}}</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>
{{{range $k, $h := .Hosts}}}
{{{$h.Alias}}}<br>
{{{end}}}
</td>
<td>
{{{if eq .Level 1}}}
{{{if eq .Status 1}}}<span><i class="checkmark big icon"></i></span>{{{else}}}<span><i class="minus big icon"></i><span>{{{end}}}
{{{end}}}
</td>
<td>
<div class="ui buttons operation">
{{{if $.IsAdmin}}}
<a href="/task/edit/{{{.Id}}}" ><i class="edit big icon" title="编辑"></i></a>&nbsp;&nbsp;&nbsp;&nbsp;
{{{if eq .Level 1}}}
{{{if eq .Status 1}}}
<a href="javascript:void(0);" @click="changeStatus({{{.Id}}},{{{.Status}}})"><i class="pause circle big icon" title="停止"></i></a>&nbsp;&nbsp;
{{{else}}}
<a href="javascript:void(0);" @click="changeStatus({{{.Id}}},{{{.Status}}})"><i class="play big icon" title="激活"></i></a>&nbsp;&nbsp;
{{{end}}}
{{{end}}}
<a href="javascript:void(0);" @click="remove({{{.Id}}})"><i class="remove big icon" title="删除"></i></a>
<a href="javascript:void(0);" @click="run({{{.Id}}})"><i class="rocket big icon" title="手动执行"></i></a>&nbsp;&nbsp;
{{{end}}}
<a href="/task/log?task_id={{{.Id}}}"><i class="bar chart icon big" title="查看日志"></i></a>
</div>
</td>
</tr>
{{{end}}}
</tbody>
</table>
{{{ template "common/pagination" .}}}
</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');
@@ -123,9 +202,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" . }}}
+8 -16
View File
@@ -13,6 +13,7 @@
{{{ template "task/menu" . }}}
<div class="twelve wide column">
{{{if .IsAdmin}}}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
@@ -22,6 +23,7 @@
</h3>
</div>
</div>
{{{end}}}
<form class="ui form">
<div class="six fields search">
<div class="field">
@@ -30,8 +32,7 @@
<div class="field">
<select name="protocol" id="protocol">
<option value="0">执行方式</option>
<option value="3" {{{if eq .Params.Protocol 3}}}selected{{{end}}}>系统命令</option>
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SSH</option>
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="1" {{{if eq .Params.Protocol 1}}}selected{{{end}}}>HTTP</option>
</select>
</div>
@@ -42,7 +43,6 @@
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
<option value="4" {{{if eq .Params.Status 3}}}selected{{{end}}}>取消</option>
<option value="5" {{{if eq .Params.Status 4}}}selected{{{end}}}>后台运行</option>
</select>
</div>
<div class="field">
@@ -50,16 +50,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>
<th>执行结果</th>
@@ -71,10 +70,9 @@
<td><a href="/task?id={{{.TaskId}}}">{{{.TaskId}}}</a></td>
<td>{{{.Name}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else}}} 系统命令 {{{end}}}</td>
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</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>
@@ -93,8 +91,6 @@
<span style="color:red">失败</span>
{{{else if eq .Status 3}}}
<span style="color:#4499EE">取消</span>
{{{else if eq .Status 4}}}
<span style="color:#43A102">后台运行</span>
{{{end}}}
</td>
<td>
@@ -133,10 +129,6 @@
</script>
<script type="text/javascript">
function showTime(startTime, endTime, status) {
}
function showResult(name, command,result) {
$('.message').html($('#task-result').html());
new Vue(
+2 -2
View File
@@ -2,10 +2,10 @@
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/task"}}}active teal{{{end}}} item" href="/task">
<i class="tasks icon"></i> 任务列表
<i class="tasks icon"></i> 定时任务列表
</a>
<a class="item {{{if eq .URI "/task/log"}}}active teal{{{end}}} " href="/task/log">
<i class="bar chart icon"></i> 任务日志
<i class="bar chart icon"></i> 定时任务日志
</a>
</div>
</div>
+129 -47
View File
@@ -19,19 +19,72 @@
<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 class="field">
<label>
<div class="content">标签名称</div>
</label>
<div class="ui small input">
<input type="text" name="tag" placeholder="标签用于任务分类" value="{{{.Task.Tag}}}">
</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,24 +92,23 @@
<div class="field">
<label>执行方式</label>
<select name="protocol" id="protocol">
<option value="3" {{{if .Task}}} {{{if eq .Task.Protocol 3}}}selected{{{end}}} {{{end}}}>系统命令</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.Protocol 2}}}selected{{{end}}} {{{end}}} data-match="host_id" data-validate-type="selectProtocol">SSH</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>
<div class="ui blue message">
<pre>选择SSH协议时需选择执行主机</pre>
</div>
<select name="host_id" id="hostId">
<option value="">选择主机</option>
<label>选择任务节点</label>
<div id="hostId">
{{{range $i, $v := .Hosts}}}
<option value="{{{.Id}}}" {{{if $.Task}}}{{{if eq $.Task.HostId .Id }}} selected {{{end}}} {{{end}}}>{{{.Alias}}}-{{{.Name}}}</option>
<label>
<input type="checkbox" value="{{{.Id}}}" {{{if $.Task}}}{{{if $v.Selected}}} checked {{{end}}}{{{end}}} style="width:25px;height: 25px;">{{{.Alias}}}-{{{.Name}}}
{{{if (HostFormat $i) }}}<br>{{{end}}}
</label>
{{{end}}}
</select> &nbsp; <a class="ui blue button" href="/host/create" target="_blank">添加主机</a>
</div> &nbsp; <br> <a class="ui blue button" href="/host/create" target="_blank">添加节点</a>
</div>
</div>
@@ -66,26 +118,23 @@
<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="{{{.Task.Timeout}}}">
</div>
</div>
<div class="six fields">
<div class="field">
<label>任务失败重试次数</label>
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{{{.Task.RetryTimes}}}">
</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">
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}></option>
<option value="2" {{{if .Task}}} {{{if eq .Task.Multi 0}}}selected{{{end}}} {{{end}}}></option>
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}></option>
</select>
</div>
</div>
<div class="three fields">
<div class="field">
@@ -111,7 +160,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>
@@ -148,6 +197,7 @@
<script type="text/javascript">
$(function() {
changeCommandPlaceholder();
changeLevel();
changeProtocol();
showNotify();
});
@@ -157,7 +207,9 @@
changeProtocol();
});
$('#level').change(function() {
changeLevel();
});
$('#task-status').change(function() {
var selected = $(this).val();
@@ -183,9 +235,6 @@
case '2':
$('#command').attr('placeholder', '请输入shell命令');
break;
case '3':
$('#command').attr('placeholder', '请输入系统命令');
break;
}
}
@@ -276,6 +325,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(
@@ -286,6 +357,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"
});
@@ -306,19 +383,6 @@
}
]
},
spec: {
identifier : 'spec',
rules: [
{
type : 'empty',
prompt : '请输入crontab格式表达式'
},
{
type : 'maxLength[64]',
prompt : '长度不能超过64'
}
]
},
command: {
identifier : 'command',
rules: [
@@ -341,6 +405,24 @@
}
]
},
timeout: {
identifier : 'timeout',
rules: [
{
type : 'integer[0..86400]',
prompt : '超时范围0-86400'
}
]
},
retryTimes: {
identifier : 'retry_times',
rules: [
{
type : 'integer[0..10]',
prompt : '重试次数0-10'
}
]
},
remark: {
identifier : 'remark',
rules: [
+73
View File
@@ -0,0 +1,73 @@
{{{ template "common/header" . }}}
<div class="ui small modal">
<div class="header">修改密码</div>
<div class="content">
<form class="ui form">
<div class="two fields">
<div class="field">
<label>原密码</label>
<input class="small input" type="password" name="old_password" placeholder="原密码">
</div>
</div>
<div class="two fields">
<div class="field">
<label>新密码</label>
<input type="password" name="new_password" placeholder="新密码">
</div>
</div>
<div class="two fields">
<div class="field">
<label>确认新密码</label>
<input type="password" name="confirm_new_password" placeholder="确认新密码">
</div>
</div>
<button class="ui button primary button" >确定</button>
</form>
</div>
</div>
<script type="text/javascript">
$('.ui.modal').modal('setting', 'closable', false).modal('show');
$('.ui.form').form(
{
onSuccess: function(event, fields) {
util.post('/user/editMyPassword', fields, function(code, message) {
swal("操作成功", '修改成功', 'success');
location.href = "/task"
});
return false;
},
fields: {
oldPassword: {
identifier : 'old_password',
rules: [
{
type : 'empty',
prompt : '请输入原密码'
}
]
},
newPassword: {
identifier : 'new_password',
rules: [
{
type : 'empty',
prompt : '请输入新密码'
}
]
},
confirmNewPassword: {
identifier : 'confirm_new_password',
rules: [
{
type : 'match[new_password]',
prompt : '两次输入密码不匹配'
}
]
}
},
inline : true
});
</script>
{{{ template "common/footer" . }}}
+3 -18
View File
@@ -3,12 +3,6 @@
<div class="header">修改密码</div>
<div class="content">
<form class="ui form">
<div class="two fields">
<div class="field">
<label>原密码</label>
<input class="small input" type="password" name="old_password" placeholder="原密码">
</div>
</div>
<div class="two fields">
<div class="field">
<label>新密码</label>
@@ -21,7 +15,7 @@
<input type="password" name="confirm_new_password" placeholder="确认新密码">
</div>
</div>
<button class="ui button primary button" >登录</button>
<button class="ui button primary button" >确定</button>
</form>
</div>
</div>
@@ -31,23 +25,14 @@
$('.ui.form').form(
{
onSuccess: function(event, fields) {
util.post('/user/editPassword', fields, function(code, message) {
util.post('/user/editPassword/{{{.Id}}}', fields, function(code, message) {
swal("操作成功", '修改成功', 'success');
location.href = "/task"
location.href = "/user"
});
return false;
},
fields: {
oldPassword: {
identifier : 'old_password',
rules: [
{
type : 'empty',
prompt : '请输入原密码'
}
]
},
newPassword: {
identifier : 'new_password',
rules: [
+73
View File
@@ -0,0 +1,73 @@
{{{ template "common/header" . }}}
<div class="ui grid">
{{{ template "user/menu" . }}}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<a href="/user/create">
<i class="large add icon"></i>
<div class="content">
添加用户
</div>
</a>
</h3>
</div>
</div>
<table class="ui celled table">
<thead>
<tr>
<th>用户ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{{range $i, $v := .Users}}}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{.Email}}}</td>
<td>{{{if .IsAdmin}}}管理员{{{else}}}普通用户{{{end}}}</td>
<td>{{{if .Status}}}启用{{{else}}}禁用{{{end}}}</td>
<td class="operation">
<a class="ui purple button" href="/user/edit/{{{.Id}}}">编辑</a>
{{{if eq .Status 1}}}
<button class="ui button red" onclick="changeStatus({{{.Id}}},{{{.Status}}})">禁用</button>&nbsp;&nbsp;
{{{else}}}
<button class="ui button twitter" onclick="changeStatus({{{.Id}}},{{{.Status}}})">启用</button>&nbsp;&nbsp;
{{{end}}}
<a href="/user/editPassword/{{{.Id}}}">
<button class="ui button facebook">修改密码</button>
</a>
<button class="ui positive button" onclick="util.removeConfirm('/user/remove/{{{.Id}}}')">删除</button>
</td>
</tr>
{{{end}}}
</tbody>
</table>
{{{ template "common/pagination" .}}}
</div>
</div>
<script type="text/javascript">
function changeStatus(id ,status) {
var url = '';
if (status) {
url = '/user/disable';
} else {
url = '/user/enable';
}
url += '/' + id;
util.post(url,{}, function() {
location.reload();
});
}
</script>
{{{ template "common/footer" . }}}
+19
View File
@@ -13,6 +13,12 @@
<input type="password" name="password" placeholder="密码">
</div>
</div>
<div class="two fields">
<div class="field">
{{{.Captcha.CreateHtml}}}
<input type="text" name="captcha" placeholder="验证码">
</div>
</div>
<button class="ui button primary button" >登录</button>
</form>
</div>
@@ -25,6 +31,10 @@
onSuccess: function(event, fields) {
util.post('/user/login', fields, function(code, message) {
location.href = "/"
}, function (code, message) {
if (code == 5) {
$('.captcha-img').trigger('click');
}
});
return false;
@@ -47,6 +57,15 @@
prompt : '请输入密码'
}
]
},
Captcha: {
identifier : 'captcha',
rules: [
{
type : 'empty',
prompt : '请输入验证码'
}
]
}
},
inline : true
+9
View File
@@ -0,0 +1,9 @@
<div class="four wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/user"}}}active teal{{{end}}} item" href="/user">
<i class="user icon"></i> 用户列表
</a>
</div>
</div>
</div>
+142
View File
@@ -0,0 +1,142 @@
{{{ template "common/header" . }}}
<div class="ui grid">
{{{ template "user/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 fluid vertical segment">
<input type="hidden" name="id" value="{{{.User.Id}}}">
<div class="three fields">
<div class="field">
<label>用户名</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.User.Name}}}" placeholder="请输入用户名">
</div>
</div>
<div class="field">
<label>邮箱</label>
<div class="ui small input">
<input type="text" name="email" value="{{{.User.Email}}}" placeholder="请输入邮箱">
</div>
</div>
</div>
{{{if eq .User.Id 0}}}
<div class="three fields">
<div class="field">
<label>密码</label>
<input type="password" name="password" placeholder="请输入密码">
</div>
<div class="field">
<label>确认密码</label>
<input type="password" name="confirm_password" placeholder="请再次输入密码">
</div>
</div>
{{{end}}}
<div class="three fields">
<div class="field">
<label>角色</label>
<input type="radio" name="is_admin" value="0" {{{if eq .User.IsAdmin 0}}}checked{{{end}}}> 普通用户
<input type="radio" name="is_admin" value="1" {{{if eq .User.IsAdmin 1}}}checked{{{end}}}> 管理员
</div>
<div class="field">
<label>状态</label>
<input type="radio" name="status" value="1" {{{if eq .User.Status 1}}}checked{{{end}}}> 启用
<input type="radio" name="status" value="0"
{{{if eq .User.Status 0}}}checked{{{end}}}
> 禁用
</div>
</div>
<div class="ui primary submit button">保存</div>
<a class="ui button" onclick="location.href='/user';">取消</a>
</form>
</div>
</div>
<script type="text/javascript">
var $uiForm = $('.ui.form');
$($uiForm).form(
{
onSuccess: function(event, fields) {
util.post('/user/store', fields, function(code, message) {
location.href = "/user"
});
return false;
},
fields: {
name: {
identifier : 'name',
rules: [
{
type : 'empty',
prompt : '请输入用户名'
},
{
type : 'minLength[3]',
prompt : '长度不能少于3'
},
{
type : 'maxLength[32]',
prompt : '长度不能超过32'
}
]
},
password: {
identifier : 'password',
rules: [
{
type : 'empty',
prompt : '请输入密码'
},
{
type : 'minLength[6]',
prompt : '长度不能少于6位'
}
]
},
confirmPassword: {
identifier : 'confirm_password',
rules: [
{
type : 'empty',
prompt : '请再次输入密码'
},
{
type : 'match[password]',
prompt : '两次输入密码不匹配'
}
]
},
email: {
identifier : 'email',
rules: [
{
type : 'empty',
prompt : '请输入邮箱'
},
{
type : 'email',
prompt : '邮箱格式错误'
}
]
}
},
inline : true
});
</script>
{{{ template "common/footer" . }}}
+6 -2
View File
@@ -11,7 +11,11 @@ fi
for i in linux darwin windows
do
./build.sh -p $i
if [[ ! $? ]];then
if [[ $? != 0 ]];then
break
fi
./build_node.sh -p $i
if [[ $? != 0 ]];then
break
fi
done
@@ -25,7 +29,7 @@ do
# 上传文件 qrsctl put bucket key srcFile
KEY=gocron/$i
qrsctl put github $KEY $i
if [[ ! $? ]];then
if [[ $? != 0 ]];then
break
fi
echo "刷新七牛CDN-" $QINIU_URL/$KEY
+2 -2
View File
@@ -14,12 +14,12 @@
package com
// PowInt is int type of math.Pow function.
// PowInt is int type of math.Pow function.
func PowInt(x int, y int) int {
if y <= 0 {
return 1
} else {
if y % 2 == 0 {
if y%2 == 0 {
sqrt := PowInt(x, y/2)
return sqrt * sqrt
} else {
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+20
View File
@@ -0,0 +1,20 @@
# cache [![Build Status](https://travis-ci.org/go-macaron/cache.svg?branch=master)](https://travis-ci.org/go-macaron/cache) [![](http://gocover.io/_badge/github.com/go-macaron/cache)](http://gocover.io/github.com/go-macaron/cache)
Middleware cache provides cache management for [Macaron](https://github.com/go-macaron/macaron). It can use many cache adapters, including memory, file, Redis, Memcache, PostgreSQL, MySQL, Ledis and Nodb.
### Installation
go get github.com/go-macaron/cache
## Getting Help
- [API Reference](https://gowalker.org/github.com/go-macaron/cache)
- [Documentation](http://go-macaron.com/docs/middlewares/cache)
## Credits
This package is a modified version of [beego/cache](https://github.com/astaxie/beego/tree/master/cache).
## License
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package cache is a middleware that provides the cache management of Macaron.
package cache
import (
"fmt"
"gopkg.in/macaron.v1"
)
const _VERSION = "0.3.0"
func Version() string {
return _VERSION
}
// Cache is the interface that operates the cache data.
type Cache interface {
// Put puts value into cache with key and expire time.
Put(key string, val interface{}, timeout int64) error
// Get gets cached value by given key.
Get(key string) interface{}
// Delete deletes cached value by given key.
Delete(key string) error
// Incr increases cached int-type value by given key as a counter.
Incr(key string) error
// Decr decreases cached int-type value by given key as a counter.
Decr(key string) error
// IsExist returns true if cached value exists.
IsExist(key string) bool
// Flush deletes all cached data.
Flush() error
// StartAndGC starts GC routine based on config string settings.
StartAndGC(opt Options) error
}
// Options represents a struct for specifying configuration options for the cache middleware.
type Options struct {
// Name of adapter. Default is "memory".
Adapter string
// Adapter configuration, it's corresponding to adapter.
AdapterConfig string
// GC interval time in seconds. Default is 60.
Interval int
// Occupy entire database. Default is false.
OccupyMode bool
// Configuration section name. Default is "cache".
Section string
}
func prepareOptions(options []Options) Options {
var opt Options
if len(options) > 0 {
opt = options[0]
}
if len(opt.Section) == 0 {
opt.Section = "cache"
}
sec := macaron.Config().Section(opt.Section)
if len(opt.Adapter) == 0 {
opt.Adapter = sec.Key("ADAPTER").MustString("memory")
}
if opt.Interval == 0 {
opt.Interval = sec.Key("INTERVAL").MustInt(60)
}
if len(opt.AdapterConfig) == 0 {
opt.AdapterConfig = sec.Key("ADAPTER_CONFIG").MustString("data/caches")
}
return opt
}
// NewCacher creates and returns a new cacher by given adapter name and configuration.
// It panics when given adapter isn't registered and starts GC automatically.
func NewCacher(name string, opt Options) (Cache, error) {
adapter, ok := adapters[name]
if !ok {
return nil, fmt.Errorf("cache: unknown adapter '%s'(forgot to import?)", name)
}
return adapter, adapter.StartAndGC(opt)
}
// Cacher is a middleware that maps a cache.Cache service into the Macaron handler chain.
// An single variadic cache.Options struct can be optionally provided to configure.
func Cacher(options ...Options) macaron.Handler {
opt := prepareOptions(options)
cache, err := NewCacher(opt.Adapter, opt)
if err != nil {
panic(err)
}
return func(ctx *macaron.Context) {
ctx.Map(cache)
}
}
var adapters = make(map[string]Cache)
// Register registers a adapter.
func Register(name string, adapter Cache) {
if adapter == nil {
panic("cache: cannot register adapter with nil value")
}
if _, dup := adapters[name]; dup {
panic(fmt.Errorf("cache: cannot register adapter '%s' twice", name))
}
adapters[name] = adapter
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sync"
"time"
"github.com/Unknwon/com"
"gopkg.in/macaron.v1"
)
// Item represents a cache item.
type Item struct {
Val interface{}
Created int64
Expire int64
}
func (item *Item) hasExpired() bool {
return item.Expire > 0 &&
(time.Now().Unix()-item.Created) >= item.Expire
}
// FileCacher represents a file cache adapter implementation.
type FileCacher struct {
lock sync.Mutex
rootPath string
interval int // GC interval.
}
// NewFileCacher creates and returns a new file cacher.
func NewFileCacher() *FileCacher {
return &FileCacher{}
}
func (c *FileCacher) filepath(key string) string {
m := md5.Sum([]byte(key))
hash := hex.EncodeToString(m[:])
return filepath.Join(c.rootPath, string(hash[0]), string(hash[1]), hash)
}
// Put puts value into cache with key and expire time.
// If expired is 0, it will be deleted by next GC operation.
func (c *FileCacher) Put(key string, val interface{}, expire int64) error {
filename := c.filepath(key)
item := &Item{val, time.Now().Unix(), expire}
data, err := EncodeGob(item)
if err != nil {
return err
}
os.MkdirAll(filepath.Dir(filename), os.ModePerm)
return ioutil.WriteFile(filename, data, os.ModePerm)
}
func (c *FileCacher) read(key string) (*Item, error) {
filename := c.filepath(key)
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
item := new(Item)
return item, DecodeGob(data, item)
}
// Get gets cached value by given key.
func (c *FileCacher) Get(key string) interface{} {
item, err := c.read(key)
if err != nil {
return nil
}
if item.hasExpired() {
os.Remove(c.filepath(key))
return nil
}
return item.Val
}
// Delete deletes cached value by given key.
func (c *FileCacher) Delete(key string) error {
return os.Remove(c.filepath(key))
}
// Incr increases cached int-type value by given key as a counter.
func (c *FileCacher) Incr(key string) error {
item, err := c.read(key)
if err != nil {
return err
}
item.Val, err = Incr(item.Val)
if err != nil {
return err
}
return c.Put(key, item.Val, item.Expire)
}
// Decrease cached int value.
func (c *FileCacher) Decr(key string) error {
item, err := c.read(key)
if err != nil {
return err
}
item.Val, err = Decr(item.Val)
if err != nil {
return err
}
return c.Put(key, item.Val, item.Expire)
}
// IsExist returns true if cached value exists.
func (c *FileCacher) IsExist(key string) bool {
return com.IsExist(c.filepath(key))
}
// Flush deletes all cached data.
func (c *FileCacher) Flush() error {
return os.RemoveAll(c.rootPath)
}
func (c *FileCacher) startGC() {
c.lock.Lock()
defer c.lock.Unlock()
if c.interval < 1 {
return
}
if err := filepath.Walk(c.rootPath, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("Walk: %v", err)
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
fmt.Errorf("ReadFile: %v", err)
}
item := new(Item)
if err = DecodeGob(data, item); err != nil {
return err
}
if item.hasExpired() {
if err = os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("Remove: %v", err)
}
}
return nil
}); err != nil {
log.Printf("error garbage collecting cache files: %v", err)
}
time.AfterFunc(time.Duration(c.interval)*time.Second, func() { c.startGC() })
}
// StartAndGC starts GC routine based on config string settings.
func (c *FileCacher) StartAndGC(opt Options) error {
c.lock.Lock()
c.rootPath = opt.AdapterConfig
c.interval = opt.Interval
if !filepath.IsAbs(c.rootPath) {
c.rootPath = filepath.Join(macaron.Root, c.rootPath)
}
c.lock.Unlock()
if err := os.MkdirAll(c.rootPath, os.ModePerm); err != nil {
return err
}
go c.startGC()
return nil
}
func init() {
Register("file", NewFileCacher())
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"errors"
"sync"
"time"
)
// MemoryItem represents a memory cache item.
type MemoryItem struct {
val interface{}
created int64
expire int64
}
func (item *MemoryItem) hasExpired() bool {
return item.expire > 0 &&
(time.Now().Unix()-item.created) >= item.expire
}
// MemoryCacher represents a memory cache adapter implementation.
type MemoryCacher struct {
lock sync.RWMutex
items map[string]*MemoryItem
interval int // GC interval.
}
// NewMemoryCacher creates and returns a new memory cacher.
func NewMemoryCacher() *MemoryCacher {
return &MemoryCacher{items: make(map[string]*MemoryItem)}
}
// Put puts value into cache with key and expire time.
// If expired is 0, it will be deleted by next GC operation.
func (c *MemoryCacher) Put(key string, val interface{}, expire int64) error {
c.lock.Lock()
defer c.lock.Unlock()
c.items[key] = &MemoryItem{
val: val,
created: time.Now().Unix(),
expire: expire,
}
return nil
}
// Get gets cached value by given key.
func (c *MemoryCacher) Get(key string) interface{} {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
if !ok {
return nil
}
if item.hasExpired() {
go c.Delete(key)
return nil
}
return item.val
}
// Delete deletes cached value by given key.
func (c *MemoryCacher) Delete(key string) error {
c.lock.Lock()
defer c.lock.Unlock()
delete(c.items, key)
return nil
}
// Incr increases cached int-type value by given key as a counter.
func (c *MemoryCacher) Incr(key string) (err error) {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
if !ok {
return errors.New("key not exist")
}
item.val, err = Incr(item.val)
return err
}
// Decr decreases cached int-type value by given key as a counter.
func (c *MemoryCacher) Decr(key string) (err error) {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
if !ok {
return errors.New("key not exist")
}
item.val, err = Decr(item.val)
return err
}
// IsExist returns true if cached value exists.
func (c *MemoryCacher) IsExist(key string) bool {
c.lock.RLock()
defer c.lock.RUnlock()
_, ok := c.items[key]
return ok
}
// Flush deletes all cached data.
func (c *MemoryCacher) Flush() error {
c.lock.Lock()
defer c.lock.Unlock()
c.items = make(map[string]*MemoryItem)
return nil
}
func (c *MemoryCacher) checkRawExpiration(key string) {
item, ok := c.items[key]
if !ok {
return
}
if item.hasExpired() {
delete(c.items, key)
}
}
func (c *MemoryCacher) checkExpiration(key string) {
c.lock.Lock()
defer c.lock.Unlock()
c.checkRawExpiration(key)
}
func (c *MemoryCacher) startGC() {
c.lock.Lock()
defer c.lock.Unlock()
if c.interval < 1 {
return
}
if c.items != nil {
for key, _ := range c.items {
c.checkRawExpiration(key)
}
}
time.AfterFunc(time.Duration(c.interval)*time.Second, func() { c.startGC() })
}
// StartAndGC starts GC routine based on config string settings.
func (c *MemoryCacher) StartAndGC(opt Options) error {
c.lock.Lock()
c.interval = opt.Interval
c.lock.Unlock()
go c.startGC()
return nil
}
func init() {
Register("memory", NewMemoryCacher())
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"bytes"
"encoding/gob"
"errors"
)
func EncodeGob(item *Item) ([]byte, error) {
buf := bytes.NewBuffer(nil)
err := gob.NewEncoder(buf).Encode(item)
return buf.Bytes(), err
}
func DecodeGob(data []byte, out *Item) error {
buf := bytes.NewBuffer(data)
return gob.NewDecoder(buf).Decode(&out)
}
func Incr(val interface{}) (interface{}, error) {
switch val.(type) {
case int:
val = val.(int) + 1
case int32:
val = val.(int32) + 1
case int64:
val = val.(int64) + 1
case uint:
val = val.(uint) + 1
case uint32:
val = val.(uint32) + 1
case uint64:
val = val.(uint64) + 1
default:
return val, errors.New("item value is not int-type")
}
return val, nil
}
func Decr(val interface{}) (interface{}, error) {
switch val.(type) {
case int:
val = val.(int) - 1
case int32:
val = val.(int32) - 1
case int64:
val = val.(int64) - 1
case uint:
if val.(uint) > 0 {
val = val.(uint) - 1
} else {
return val, errors.New("item value is less than 0")
}
case uint32:
if val.(uint32) > 0 {
val = val.(uint32) - 1
} else {
return val, errors.New("item value is less than 0")
}
case uint64:
if val.(uint64) > 0 {
val = val.(uint64) - 1
} else {
return val, errors.New("item value is less than 0")
}
default:
return val, errors.New("item value is not int-type")
}
return val, nil
}
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+16
View File
@@ -0,0 +1,16 @@
# captcha [![Build Status](https://travis-ci.org/go-macaron/captcha.svg?branch=master)](https://travis-ci.org/go-macaron/captcha)
Middleware captcha provides captcha service for [Macaron](https://github.com/go-macaron/macaron).
### Installation
go get github.com/go-macaron/captcha
## Getting Help
- [API Reference](https://gowalker.org/github.com/go-macaron/captcha)
- [Documentation](http://go-macaron.com/docs/middlewares/captcha)
## License
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
+246
View File
@@ -0,0 +1,246 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package captcha a middleware that provides captcha service for Macaron.
package captcha
import (
"fmt"
"html/template"
"path"
"strings"
"github.com/Unknwon/com"
"github.com/go-macaron/cache"
"gopkg.in/macaron.v1"
)
const _VERSION = "0.1.0"
func Version() string {
return _VERSION
}
var (
defaultChars = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
)
// Captcha represents a captcha service.
type Captcha struct {
store cache.Cache
SubURL string
URLPrefix string
FieldIdName string
FieldCaptchaName string
StdWidth int
StdHeight int
ChallengeNums int
Expiration int64
CachePrefix string
}
// generate key string
func (c *Captcha) key(id string) string {
return c.CachePrefix + id
}
// generate rand chars with default chars
func (c *Captcha) genRandChars() string {
return string(com.RandomCreateBytes(c.ChallengeNums, defaultChars...))
}
// CreateHTML outputs HTML for display and fetch new captcha images.
func (c *Captcha) CreateHTML() template.HTML {
value, err := c.CreateCaptcha()
if err != nil {
panic(fmt.Errorf("fail to create captcha: %v", err))
}
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%[1]s" value="%[2]s">
<a class="captcha" href="javascript:" tabindex="-1">
<img onclick="this.src=('%[3]s%[4]s%[2]s.png?reload='+(new Date()).getTime())" class="captcha-img" src="%[3]s%[4]s%[2]s.png">
</a>`, c.FieldIdName, value, c.SubURL, c.URLPrefix))
}
// DEPRECATED
func (c *Captcha) CreateHtml() template.HTML {
return c.CreateHTML()
}
// create a new captcha id
func (c *Captcha) CreateCaptcha() (string, error) {
id := string(com.RandomCreateBytes(15))
if err := c.store.Put(c.key(id), c.genRandChars(), c.Expiration); err != nil {
return "", err
}
return id, nil
}
// verify from a request
func (c *Captcha) VerifyReq(req macaron.Request) bool {
req.ParseForm()
return c.Verify(req.Form.Get(c.FieldIdName), req.Form.Get(c.FieldCaptchaName))
}
// direct verify id and challenge string
func (c *Captcha) Verify(id string, challenge string) bool {
if len(challenge) == 0 || len(id) == 0 {
return false
}
var chars string
key := c.key(id)
if v, ok := c.store.Get(key).(string); ok {
chars = v
} else {
return false
}
defer c.store.Delete(key)
if len(chars) != len(challenge) {
return false
}
// verify challenge
for i, c := range []byte(chars) {
if c != challenge[i]-48 {
return false
}
}
return true
}
type Options struct {
// Suburl path. Default is empty.
SubURL string
// URL prefix of getting captcha pictures. Default is "/captcha/".
URLPrefix string
// Hidden input element ID. Default is "captcha_id".
FieldIdName string
// User input value element name in request form. Default is "captcha".
FieldCaptchaName string
// Challenge number. Default is 6.
ChallengeNums int
// Captcha image width. Default is 240.
Width int
// Captcha image height. Default is 80.
Height int
// Captcha expiration time in seconds. Default is 600.
Expiration int64
// Cache key prefix captcha characters. Default is "captcha_".
CachePrefix string
}
func prepareOptions(options []Options) Options {
var opt Options
if len(options) > 0 {
opt = options[0]
}
opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
// Defaults.
if len(opt.URLPrefix) == 0 {
opt.URLPrefix = "/captcha/"
} else if opt.URLPrefix[len(opt.URLPrefix)-1] != '/' {
opt.URLPrefix += "/"
}
if len(opt.FieldIdName) == 0 {
opt.FieldIdName = "captcha_id"
}
if len(opt.FieldCaptchaName) == 0 {
opt.FieldCaptchaName = "captcha"
}
if opt.ChallengeNums == 0 {
opt.ChallengeNums = 6
}
if opt.Width == 0 {
opt.Width = stdWidth
}
if opt.Height == 0 {
opt.Height = stdHeight
}
if opt.Expiration == 0 {
opt.Expiration = 600
}
if len(opt.CachePrefix) == 0 {
opt.CachePrefix = "captcha_"
}
return opt
}
// NewCaptcha initializes and returns a captcha with given options.
func NewCaptcha(opt Options) *Captcha {
return &Captcha{
SubURL: opt.SubURL,
URLPrefix: opt.URLPrefix,
FieldIdName: opt.FieldIdName,
FieldCaptchaName: opt.FieldCaptchaName,
StdWidth: opt.Width,
StdHeight: opt.Height,
ChallengeNums: opt.ChallengeNums,
Expiration: opt.Expiration,
CachePrefix: opt.CachePrefix,
}
}
// Captchaer is a middleware that maps a captcha.Captcha service into the Macaron handler chain.
// An single variadic captcha.Options struct can be optionally provided to configure.
// This should be register after cache.Cacher.
func Captchaer(options ...Options) macaron.Handler {
return func(ctx *macaron.Context, cache cache.Cache) {
cpt := NewCaptcha(prepareOptions(options))
cpt.store = cache
if strings.HasPrefix(ctx.Req.URL.Path, cpt.URLPrefix) {
var chars string
id := path.Base(ctx.Req.URL.Path)
if i := strings.Index(id, "."); i > -1 {
id = id[:i]
}
key := cpt.key(id)
// Reload captcha.
if len(ctx.Query("reload")) > 0 {
chars = cpt.genRandChars()
if err := cpt.store.Put(key, chars, cpt.Expiration); err != nil {
ctx.Status(500)
ctx.Write([]byte("captcha reload error"))
panic(fmt.Errorf("reload captcha: %v", err))
}
} else {
if v, ok := cpt.store.Get(key).(string); ok {
chars = v
} else {
ctx.Status(404)
ctx.Write([]byte("captcha not found"))
return
}
}
if _, err := NewImage([]byte(chars), cpt.StdWidth, cpt.StdHeight).WriteTo(ctx.Resp); err != nil {
panic(fmt.Errorf("write captcha: %v", err))
}
return
}
ctx.Data["Captcha"] = cpt
ctx.Map(cpt)
}
}
+498
View File
@@ -0,0 +1,498 @@
// Copyright 2013 Beego Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package captcha
import (
"bytes"
"image"
"image/color"
"image/png"
"io"
"math"
)
const (
fontWidth = 11
fontHeight = 18
blackChar = 1
// Standard width and height of a captcha image.
stdWidth = 240
stdHeight = 80
// Maximum absolute skew factor of a single digit.
maxSkew = 0.7
// Number of background circles.
circleCount = 20
)
var font = [][]byte{
{ // 0
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 1
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0,
0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
{ // 2
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
{ // 3
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 4
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0,
1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
},
{ // 5
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 6
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0,
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 7
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
},
{ // 8
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 9
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
},
}
type Image struct {
*image.Paletted
numWidth int
numHeight int
dotSize int
}
var prng = &siprng{}
// randIntn returns a pseudorandom non-negative int in range [0, n).
func randIntn(n int) int {
return prng.Intn(n)
}
// randInt returns a pseudorandom int in range [from, to].
func randInt(from, to int) int {
return prng.Intn(to+1-from) + from
}
// randFloat returns a pseudorandom float64 in range [from, to].
func randFloat(from, to float64) float64 {
return (to-from)*prng.Float64() + from
}
func randomPalette() color.Palette {
p := make([]color.Color, circleCount+1)
// Transparent color.
p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
// Primary color.
prim := color.RGBA{
uint8(randIntn(129)),
uint8(randIntn(129)),
uint8(randIntn(129)),
0xFF,
}
p[1] = prim
// Circle colors.
for i := 2; i <= circleCount; i++ {
p[i] = randomBrightness(prim, 255)
}
return p
}
// NewImage returns a new captcha image of the given width and height with the
// given digits, where each digit must be in range 0-9.
func NewImage(digits []byte, width, height int) *Image {
m := new(Image)
m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), randomPalette())
m.calculateSizes(width, height, len(digits))
// Randomly position captcha inside the image.
maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
maxy := height - m.numHeight - m.dotSize*2
var border int
if width > height {
border = height / 5
} else {
border = width / 5
}
x := randInt(border, maxx-border)
y := randInt(border, maxy-border)
// Draw digits.
for _, n := range digits {
m.drawDigit(font[n], x, y)
x += m.numWidth + m.dotSize
}
// Draw strike-through line.
m.strikeThrough()
// Apply wave distortion.
m.distort(randFloat(5, 10), randFloat(100, 200))
// Fill image with random circles.
m.fillWithCircles(circleCount, m.dotSize)
return m
}
// encodedPNG encodes an image to PNG and returns
// the result as a byte slice.
func (m *Image) encodedPNG() []byte {
var buf bytes.Buffer
if err := png.Encode(&buf, m.Paletted); err != nil {
panic(err.Error())
}
return buf.Bytes()
}
// WriteTo writes captcha image in PNG format into the given writer.
func (m *Image) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(m.encodedPNG())
return int64(n), err
}
func (m *Image) calculateSizes(width, height, ncount int) {
// Goal: fit all digits inside the image.
var border int
if width > height {
border = height / 4
} else {
border = width / 4
}
// Convert everything to floats for calculations.
w := float64(width - border*2)
h := float64(height - border*2)
// fw takes into account 1-dot spacing between digits.
fw := float64(fontWidth + 1)
fh := float64(fontHeight)
nc := float64(ncount)
// Calculate the width of a single digit taking into account only the
// width of the image.
nw := w / nc
// Calculate the height of a digit from this width.
nh := nw * fh / fw
// Digit too high?
if nh > h {
// Fit digits based on height.
nh = h
nw = fw / fh * nh
}
// Calculate dot size.
m.dotSize = int(nh / fh)
// Save everything, making the actual width smaller by 1 dot to account
// for spacing between digits.
m.numWidth = int(nw) - m.dotSize
m.numHeight = int(nh)
}
func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
for x := fromX; x <= toX; x++ {
m.SetColorIndex(x, y, colorIdx)
}
}
func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
f := 1 - radius
dfx := 1
dfy := -2 * radius
xo := 0
yo := radius
m.SetColorIndex(x, y+radius, colorIdx)
m.SetColorIndex(x, y-radius, colorIdx)
m.drawHorizLine(x-radius, x+radius, y, colorIdx)
for xo < yo {
if f >= 0 {
yo--
dfy += 2
f += dfy
}
xo++
dfx += 2
f += dfx
m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
}
}
func (m *Image) fillWithCircles(n, maxradius int) {
maxx := m.Bounds().Max.X
maxy := m.Bounds().Max.Y
for i := 0; i < n; i++ {
colorIdx := uint8(randInt(1, circleCount-1))
r := randInt(1, maxradius)
m.drawCircle(randInt(r, maxx-r), randInt(r, maxy-r), r, colorIdx)
}
}
func (m *Image) strikeThrough() {
maxx := m.Bounds().Max.X
maxy := m.Bounds().Max.Y
y := randInt(maxy/3, maxy-maxy/3)
amplitude := randFloat(5, 20)
period := randFloat(80, 180)
dx := 2.0 * math.Pi / period
for x := 0; x < maxx; x++ {
xo := amplitude * math.Cos(float64(y)*dx)
yo := amplitude * math.Sin(float64(x)*dx)
for yn := 0; yn < m.dotSize; yn++ {
r := randInt(0, m.dotSize)
m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
}
}
}
func (m *Image) drawDigit(digit []byte, x, y int) {
skf := randFloat(-maxSkew, maxSkew)
xs := float64(x)
r := m.dotSize / 2
y += randInt(-r, r)
for yo := 0; yo < fontHeight; yo++ {
for xo := 0; xo < fontWidth; xo++ {
if digit[yo*fontWidth+xo] != blackChar {
continue
}
m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
}
xs += skf
x = int(xs)
}
}
func (m *Image) distort(amplude float64, period float64) {
w := m.Bounds().Max.X
h := m.Bounds().Max.Y
oldm := m.Paletted
newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
dx := 2.0 * math.Pi / period
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
xo := amplude * math.Sin(float64(y)*dx)
yo := amplude * math.Cos(float64(x)*dx)
newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
}
}
m.Paletted = newm
}
func randomBrightness(c color.RGBA, max uint8) color.RGBA {
minc := min3(c.R, c.G, c.B)
maxc := max3(c.R, c.G, c.B)
if maxc > max {
return c
}
n := randIntn(int(max-maxc)) - int(minc)
return color.RGBA{
uint8(int(c.R) + n),
uint8(int(c.G) + n),
uint8(int(c.B) + n),
uint8(c.A),
}
}
func min3(x, y, z uint8) (m uint8) {
m = x
if y < m {
m = y
}
if z < m {
m = z
}
return
}
func max3(x, y, z uint8) (m uint8) {
m = x
if y > m {
m = y
}
if z > m {
m = z
}
return
}
+277
View File
@@ -0,0 +1,277 @@
// Copyright 2013 Beego Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package captcha
import (
"crypto/rand"
"encoding/binary"
"io"
"sync"
)
// siprng is PRNG based on SipHash-2-4.
type siprng struct {
mu sync.Mutex
k0, k1, ctr uint64
}
// siphash implements SipHash-2-4, accepting a uint64 as a message.
func siphash(k0, k1, m uint64) uint64 {
// Initialization.
v0 := k0 ^ 0x736f6d6570736575
v1 := k1 ^ 0x646f72616e646f6d
v2 := k0 ^ 0x6c7967656e657261
v3 := k1 ^ 0x7465646279746573
t := uint64(8) << 56
// Compression.
v3 ^= m
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
v0 ^= m
// Compress last block.
v3 ^= t
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
v0 ^= t
// Finalization.
v2 ^= 0xff
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 3.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 4.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
return v0 ^ v1 ^ v2 ^ v3
}
// rekey sets a new PRNG key, which is read from crypto/rand.
func (p *siprng) rekey() {
var k [16]byte
if _, err := io.ReadFull(rand.Reader, k[:]); err != nil {
panic(err.Error())
}
p.k0 = binary.LittleEndian.Uint64(k[0:8])
p.k1 = binary.LittleEndian.Uint64(k[8:16])
p.ctr = 1
}
// Uint64 returns a new pseudorandom uint64.
// It rekeys PRNG on the first call and every 64 MB of generated data.
func (p *siprng) Uint64() uint64 {
p.mu.Lock()
if p.ctr == 0 || p.ctr > 8*1024*1024 {
p.rekey()
}
v := siphash(p.k0, p.k1, p.ctr)
p.ctr++
p.mu.Unlock()
return v
}
func (p *siprng) Int63() int64 {
return int64(p.Uint64() & 0x7fffffffffffffff)
}
func (p *siprng) Uint32() uint32 {
return uint32(p.Uint64())
}
func (p *siprng) Int31() int32 {
return int32(p.Uint32() & 0x7fffffff)
}
func (p *siprng) Intn(n int) int {
if n <= 0 {
panic("invalid argument to Intn")
}
if n <= 1<<31-1 {
return int(p.Int31n(int32(n)))
}
return int(p.Int63n(int64(n)))
}
func (p *siprng) Int63n(n int64) int64 {
if n <= 0 {
panic("invalid argument to Int63n")
}
max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
v := p.Int63()
for v > max {
v = p.Int63()
}
return v % n
}
func (p *siprng) Int31n(n int32) int32 {
if n <= 0 {
panic("invalid argument to Int31n")
}
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
v := p.Int31()
for v > max {
v = p.Int31()
}
return v % n
}
func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }
+1 -1
View File
@@ -124,7 +124,7 @@ type Options struct {
// Cookie value used to set and get token.
Cookie string
// Cookie path.
CookiePath string
CookiePath string
CookieHttpOnly bool
// Key used for getting the unique ID per user.
SessionKey string
+217 -217
View File
@@ -1,217 +1,217 @@
// 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 session
import (
"container/list"
"fmt"
"sync"
"time"
)
// MemStore represents a in-memory session store implementation.
type MemStore struct {
sid string
lock sync.RWMutex
data map[interface{}]interface{}
lastAccess time.Time
}
// NewMemStore creates and returns a memory session store.
func NewMemStore(sid string) *MemStore {
return &MemStore{
sid: sid,
data: make(map[interface{}]interface{}),
lastAccess: time.Now(),
}
}
// Set sets value to given key in session.
func (s *MemStore) Set(key, val interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
s.data[key] = val
return nil
}
// Get gets value by given key in session.
func (s *MemStore) Get(key interface{}) interface{} {
s.lock.RLock()
defer s.lock.RUnlock()
return s.data[key]
}
// Delete deletes a key from session.
func (s *MemStore) Delete(key interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data, key)
return nil
}
// ID returns current session ID.
func (s *MemStore) ID() string {
return s.sid
}
// Release releases resource and save data to provider.
func (_ *MemStore) Release() error {
return nil
}
// Flush deletes all session data.
func (s *MemStore) Flush() error {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[interface{}]interface{})
return nil
}
// MemProvider represents a in-memory session provider implementation.
type MemProvider struct {
lock sync.RWMutex
maxLifetime int64
data map[string]*list.Element
// A priority list whose lastAccess newer gets higer priority.
list *list.List
}
// Init initializes memory session provider.
func (p *MemProvider) Init(maxLifetime int64, _ string) error {
p.lock.Lock()
p.maxLifetime = maxLifetime
p.lock.Unlock()
return nil
}
// update expands time of session store by given ID.
func (p *MemProvider) update(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
if e, ok := p.data[sid]; ok {
e.Value.(*MemStore).lastAccess = time.Now()
p.list.MoveToFront(e)
return nil
}
return nil
}
// Read returns raw session store by session ID.
func (p *MemProvider) Read(sid string) (_ RawStore, err error) {
p.lock.RLock()
e, ok := p.data[sid]
p.lock.RUnlock()
if ok {
if err = p.update(sid); err != nil {
return nil, err
}
return e.Value.(*MemStore), nil
}
// Create a new session.
p.lock.Lock()
defer p.lock.Unlock()
s := NewMemStore(sid)
p.data[sid] = p.list.PushBack(s)
return s, nil
}
// Exist returns true if session with given ID exists.
func (p *MemProvider) Exist(sid string) bool {
p.lock.RLock()
defer p.lock.RUnlock()
_, ok := p.data[sid]
return ok
}
// Destory deletes a session by session ID.
func (p *MemProvider) Destory(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
e, ok := p.data[sid]
if !ok {
return nil
}
p.list.Remove(e)
delete(p.data, sid)
return nil
}
// Regenerate regenerates a session store from old session ID to new one.
func (p *MemProvider) Regenerate(oldsid, sid string) (RawStore, error) {
if p.Exist(sid) {
return nil, fmt.Errorf("new sid '%s' already exists", sid)
}
s, err := p.Read(oldsid)
if err != nil {
return nil, err
}
if err = p.Destory(oldsid); err != nil {
return nil, err
}
s.(*MemStore).sid = sid
p.lock.Lock()
defer p.lock.Unlock()
p.data[sid] = p.list.PushBack(s)
return s, nil
}
// Count counts and returns number of sessions.
func (p *MemProvider) Count() int {
return p.list.Len()
}
// GC calls GC to clean expired sessions.
func (p *MemProvider) GC() {
p.lock.RLock()
for {
// No session in the list.
e := p.list.Back()
if e == nil {
break
}
if (e.Value.(*MemStore).lastAccess.Unix() + p.maxLifetime) < time.Now().Unix() {
p.lock.RUnlock()
p.lock.Lock()
p.list.Remove(e)
delete(p.data, e.Value.(*MemStore).sid)
p.lock.Unlock()
p.lock.RLock()
} else {
break
}
}
p.lock.RUnlock()
}
func init() {
Register("memory", &MemProvider{list: list.New(), data: make(map[string]*list.Element)})
}
// 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 session
import (
"container/list"
"fmt"
"sync"
"time"
)
// MemStore represents a in-memory session store implementation.
type MemStore struct {
sid string
lock sync.RWMutex
data map[interface{}]interface{}
lastAccess time.Time
}
// NewMemStore creates and returns a memory session store.
func NewMemStore(sid string) *MemStore {
return &MemStore{
sid: sid,
data: make(map[interface{}]interface{}),
lastAccess: time.Now(),
}
}
// Set sets value to given key in session.
func (s *MemStore) Set(key, val interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
s.data[key] = val
return nil
}
// Get gets value by given key in session.
func (s *MemStore) Get(key interface{}) interface{} {
s.lock.RLock()
defer s.lock.RUnlock()
return s.data[key]
}
// Delete deletes a key from session.
func (s *MemStore) Delete(key interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data, key)
return nil
}
// ID returns current session ID.
func (s *MemStore) ID() string {
return s.sid
}
// Release releases resource and save data to provider.
func (_ *MemStore) Release() error {
return nil
}
// Flush deletes all session data.
func (s *MemStore) Flush() error {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[interface{}]interface{})
return nil
}
// MemProvider represents a in-memory session provider implementation.
type MemProvider struct {
lock sync.RWMutex
maxLifetime int64
data map[string]*list.Element
// A priority list whose lastAccess newer gets higer priority.
list *list.List
}
// Init initializes memory session provider.
func (p *MemProvider) Init(maxLifetime int64, _ string) error {
p.lock.Lock()
p.maxLifetime = maxLifetime
p.lock.Unlock()
return nil
}
// update expands time of session store by given ID.
func (p *MemProvider) update(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
if e, ok := p.data[sid]; ok {
e.Value.(*MemStore).lastAccess = time.Now()
p.list.MoveToFront(e)
return nil
}
return nil
}
// Read returns raw session store by session ID.
func (p *MemProvider) Read(sid string) (_ RawStore, err error) {
p.lock.RLock()
e, ok := p.data[sid]
p.lock.RUnlock()
if ok {
if err = p.update(sid); err != nil {
return nil, err
}
return e.Value.(*MemStore), nil
}
// Create a new session.
p.lock.Lock()
defer p.lock.Unlock()
s := NewMemStore(sid)
p.data[sid] = p.list.PushBack(s)
return s, nil
}
// Exist returns true if session with given ID exists.
func (p *MemProvider) Exist(sid string) bool {
p.lock.RLock()
defer p.lock.RUnlock()
_, ok := p.data[sid]
return ok
}
// Destory deletes a session by session ID.
func (p *MemProvider) Destory(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
e, ok := p.data[sid]
if !ok {
return nil
}
p.list.Remove(e)
delete(p.data, sid)
return nil
}
// Regenerate regenerates a session store from old session ID to new one.
func (p *MemProvider) Regenerate(oldsid, sid string) (RawStore, error) {
if p.Exist(sid) {
return nil, fmt.Errorf("new sid '%s' already exists", sid)
}
s, err := p.Read(oldsid)
if err != nil {
return nil, err
}
if err = p.Destory(oldsid); err != nil {
return nil, err
}
s.(*MemStore).sid = sid
p.lock.Lock()
defer p.lock.Unlock()
p.data[sid] = p.list.PushBack(s)
return s, nil
}
// Count counts and returns number of sessions.
func (p *MemProvider) Count() int {
return p.list.Len()
}
// GC calls GC to clean expired sessions.
func (p *MemProvider) GC() {
p.lock.RLock()
for {
// No session in the list.
e := p.list.Back()
if e == nil {
break
}
if (e.Value.(*MemStore).lastAccess.Unix() + p.maxLifetime) < time.Now().Unix() {
p.lock.RUnlock()
p.lock.Lock()
p.list.Remove(e)
delete(p.data, e.Value.(*MemStore).sid)
p.lock.Unlock()
p.lock.RLock()
} else {
break
}
}
p.lock.RUnlock()
}
func init() {
Register("memory", &MemProvider{list: list.New(), data: make(map[string]*list.Element)})
}
+2 -3
View File
@@ -1,11 +1,11 @@
package core
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"time"
"bytes"
"encoding/gob"
)
const (
@@ -55,7 +55,6 @@ func encodeIds(ids []PK) (string, error) {
return buf.String(), err
}
func decodeIds(s string) ([]PK, error) {
pks := make([]PK, 0)
+3 -1
View File
@@ -252,7 +252,9 @@ func (engine *Engine) Close() error {
func (engine *Engine) Ping() error {
session := engine.NewSession()
defer session.Close()
engine.logger.Infof("PING DATABASE %v", engine.DriverName())
if engine.showSQL {
engine.logger.Infof("PING DATABASE %v", engine.DriverName())
}
return session.Ping()
}
+31
View File
@@ -0,0 +1,31 @@
Go support for Protocol Buffers - Google's data interchange format
Copyright 2010 The Go Authors. All rights reserved.
https://github.com/golang/protobuf
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+43
View File
@@ -0,0 +1,43 @@
# Go support for Protocol Buffers - Google's data interchange format
#
# Copyright 2010 The Go Authors. All rights reserved.
# https://github.com/golang/protobuf
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
install:
go install
test: install generate-test-pbs
go test
generate-test-pbs:
make install
make -C testdata
protoc --go_out=Mtestdata/test.proto=github.com/golang/protobuf/proto/testdata,Mgoogle/protobuf/any.proto=github.com/golang/protobuf/ptypes/any:. proto3_proto/proto3.proto
make
+229
View File
@@ -0,0 +1,229 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2011 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Protocol buffer deep copy and merge.
// TODO: RawMessage.
package proto
import (
"log"
"reflect"
"strings"
)
// Clone returns a deep copy of a protocol buffer.
func Clone(pb Message) Message {
in := reflect.ValueOf(pb)
if in.IsNil() {
return pb
}
out := reflect.New(in.Type().Elem())
// out is empty so a merge is a deep copy.
mergeStruct(out.Elem(), in.Elem())
return out.Interface().(Message)
}
// Merge merges src into dst.
// Required and optional fields that are set in src will be set to that value in dst.
// Elements of repeated fields will be appended.
// Merge panics if src and dst are not the same type, or if dst is nil.
func Merge(dst, src Message) {
in := reflect.ValueOf(src)
out := reflect.ValueOf(dst)
if out.IsNil() {
panic("proto: nil destination")
}
if in.Type() != out.Type() {
// Explicit test prior to mergeStruct so that mistyped nils will fail
panic("proto: type mismatch")
}
if in.IsNil() {
// Merging nil into non-nil is a quiet no-op
return
}
mergeStruct(out.Elem(), in.Elem())
}
func mergeStruct(out, in reflect.Value) {
sprop := GetProperties(in.Type())
for i := 0; i < in.NumField(); i++ {
f := in.Type().Field(i)
if strings.HasPrefix(f.Name, "XXX_") {
continue
}
mergeAny(out.Field(i), in.Field(i), false, sprop.Prop[i])
}
if emIn, ok := extendable(in.Addr().Interface()); ok {
emOut, _ := extendable(out.Addr().Interface())
mIn, muIn := emIn.extensionsRead()
if mIn != nil {
mOut := emOut.extensionsWrite()
muIn.Lock()
mergeExtension(mOut, mIn)
muIn.Unlock()
}
}
uf := in.FieldByName("XXX_unrecognized")
if !uf.IsValid() {
return
}
uin := uf.Bytes()
if len(uin) > 0 {
out.FieldByName("XXX_unrecognized").SetBytes(append([]byte(nil), uin...))
}
}
// mergeAny performs a merge between two values of the same type.
// viaPtr indicates whether the values were indirected through a pointer (implying proto2).
// prop is set if this is a struct field (it may be nil).
func mergeAny(out, in reflect.Value, viaPtr bool, prop *Properties) {
if in.Type() == protoMessageType {
if !in.IsNil() {
if out.IsNil() {
out.Set(reflect.ValueOf(Clone(in.Interface().(Message))))
} else {
Merge(out.Interface().(Message), in.Interface().(Message))
}
}
return
}
switch in.Kind() {
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64,
reflect.String, reflect.Uint32, reflect.Uint64:
if !viaPtr && isProto3Zero(in) {
return
}
out.Set(in)
case reflect.Interface:
// Probably a oneof field; copy non-nil values.
if in.IsNil() {
return
}
// Allocate destination if it is not set, or set to a different type.
// Otherwise we will merge as normal.
if out.IsNil() || out.Elem().Type() != in.Elem().Type() {
out.Set(reflect.New(in.Elem().Elem().Type())) // interface -> *T -> T -> new(T)
}
mergeAny(out.Elem(), in.Elem(), false, nil)
case reflect.Map:
if in.Len() == 0 {
return
}
if out.IsNil() {
out.Set(reflect.MakeMap(in.Type()))
}
// For maps with value types of *T or []byte we need to deep copy each value.
elemKind := in.Type().Elem().Kind()
for _, key := range in.MapKeys() {
var val reflect.Value
switch elemKind {
case reflect.Ptr:
val = reflect.New(in.Type().Elem().Elem())
mergeAny(val, in.MapIndex(key), false, nil)
case reflect.Slice:
val = in.MapIndex(key)
val = reflect.ValueOf(append([]byte{}, val.Bytes()...))
default:
val = in.MapIndex(key)
}
out.SetMapIndex(key, val)
}
case reflect.Ptr:
if in.IsNil() {
return
}
if out.IsNil() {
out.Set(reflect.New(in.Elem().Type()))
}
mergeAny(out.Elem(), in.Elem(), true, nil)
case reflect.Slice:
if in.IsNil() {
return
}
if in.Type().Elem().Kind() == reflect.Uint8 {
// []byte is a scalar bytes field, not a repeated field.
// Edge case: if this is in a proto3 message, a zero length
// bytes field is considered the zero value, and should not
// be merged.
if prop != nil && prop.proto3 && in.Len() == 0 {
return
}
// Make a deep copy.
// Append to []byte{} instead of []byte(nil) so that we never end up
// with a nil result.
out.SetBytes(append([]byte{}, in.Bytes()...))
return
}
n := in.Len()
if out.IsNil() {
out.Set(reflect.MakeSlice(in.Type(), 0, n))
}
switch in.Type().Elem().Kind() {
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64,
reflect.String, reflect.Uint32, reflect.Uint64:
out.Set(reflect.AppendSlice(out, in))
default:
for i := 0; i < n; i++ {
x := reflect.Indirect(reflect.New(in.Type().Elem()))
mergeAny(x, in.Index(i), false, nil)
out.Set(reflect.Append(out, x))
}
}
case reflect.Struct:
mergeStruct(out, in)
default:
// unknown type, so not a protocol buffer
log.Printf("proto: don't know how to copy %v", in)
}
}
func mergeExtension(out, in map[int32]Extension) {
for extNum, eIn := range in {
eOut := Extension{desc: eIn.desc}
if eIn.value != nil {
v := reflect.New(reflect.TypeOf(eIn.value)).Elem()
mergeAny(v, reflect.ValueOf(eIn.value), false, nil)
eOut.value = v.Interface()
}
if eIn.enc != nil {
eOut.enc = make([]byte, len(eIn.enc))
copy(eOut.enc, eIn.enc)
}
out[extNum] = eOut
}
}
+970
View File
@@ -0,0 +1,970 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2010 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package proto
/*
* Routines for decoding protocol buffer data to construct in-memory representations.
*/
import (
"errors"
"fmt"
"io"
"os"
"reflect"
)
// errOverflow is returned when an integer is too large to be represented.
var errOverflow = errors.New("proto: integer overflow")
// ErrInternalBadWireType is returned by generated code when an incorrect
// wire type is encountered. It does not get returned to user code.
var ErrInternalBadWireType = errors.New("proto: internal error: bad wiretype for oneof")
// The fundamental decoders that interpret bytes on the wire.
// Those that take integer types all return uint64 and are
// therefore of type valueDecoder.
// DecodeVarint reads a varint-encoded integer from the slice.
// It returns the integer and the number of bytes consumed, or
// zero if there is not enough.
// This is the format for the
// int32, int64, uint32, uint64, bool, and enum
// protocol buffer types.
func DecodeVarint(buf []byte) (x uint64, n int) {
for shift := uint(0); shift < 64; shift += 7 {
if n >= len(buf) {
return 0, 0
}
b := uint64(buf[n])
n++
x |= (b & 0x7F) << shift
if (b & 0x80) == 0 {
return x, n
}
}
// The number is too large to represent in a 64-bit value.
return 0, 0
}
func (p *Buffer) decodeVarintSlow() (x uint64, err error) {
i := p.index
l := len(p.buf)
for shift := uint(0); shift < 64; shift += 7 {
if i >= l {
err = io.ErrUnexpectedEOF
return
}
b := p.buf[i]
i++
x |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
p.index = i
return
}
}
// The number is too large to represent in a 64-bit value.
err = errOverflow
return
}
// DecodeVarint reads a varint-encoded integer from the Buffer.
// This is the format for the
// int32, int64, uint32, uint64, bool, and enum
// protocol buffer types.
func (p *Buffer) DecodeVarint() (x uint64, err error) {
i := p.index
buf := p.buf
if i >= len(buf) {
return 0, io.ErrUnexpectedEOF
} else if buf[i] < 0x80 {
p.index++
return uint64(buf[i]), nil
} else if len(buf)-i < 10 {
return p.decodeVarintSlow()
}
var b uint64
// we already checked the first byte
x = uint64(buf[i]) - 0x80
i++
b = uint64(buf[i])
i++
x += b << 7
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 7
b = uint64(buf[i])
i++
x += b << 14
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 14
b = uint64(buf[i])
i++
x += b << 21
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 21
b = uint64(buf[i])
i++
x += b << 28
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 28
b = uint64(buf[i])
i++
x += b << 35
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 35
b = uint64(buf[i])
i++
x += b << 42
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 42
b = uint64(buf[i])
i++
x += b << 49
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 49
b = uint64(buf[i])
i++
x += b << 56
if b&0x80 == 0 {
goto done
}
x -= 0x80 << 56
b = uint64(buf[i])
i++
x += b << 63
if b&0x80 == 0 {
goto done
}
// x -= 0x80 << 63 // Always zero.
return 0, errOverflow
done:
p.index = i
return x, nil
}
// DecodeFixed64 reads a 64-bit integer from the Buffer.
// This is the format for the
// fixed64, sfixed64, and double protocol buffer types.
func (p *Buffer) DecodeFixed64() (x uint64, err error) {
// x, err already 0
i := p.index + 8
if i < 0 || i > len(p.buf) {
err = io.ErrUnexpectedEOF
return
}
p.index = i
x = uint64(p.buf[i-8])
x |= uint64(p.buf[i-7]) << 8
x |= uint64(p.buf[i-6]) << 16
x |= uint64(p.buf[i-5]) << 24
x |= uint64(p.buf[i-4]) << 32
x |= uint64(p.buf[i-3]) << 40
x |= uint64(p.buf[i-2]) << 48
x |= uint64(p.buf[i-1]) << 56
return
}
// DecodeFixed32 reads a 32-bit integer from the Buffer.
// This is the format for the
// fixed32, sfixed32, and float protocol buffer types.
func (p *Buffer) DecodeFixed32() (x uint64, err error) {
// x, err already 0
i := p.index + 4
if i < 0 || i > len(p.buf) {
err = io.ErrUnexpectedEOF
return
}
p.index = i
x = uint64(p.buf[i-4])
x |= uint64(p.buf[i-3]) << 8
x |= uint64(p.buf[i-2]) << 16
x |= uint64(p.buf[i-1]) << 24
return
}
// DecodeZigzag64 reads a zigzag-encoded 64-bit integer
// from the Buffer.
// This is the format used for the sint64 protocol buffer type.
func (p *Buffer) DecodeZigzag64() (x uint64, err error) {
x, err = p.DecodeVarint()
if err != nil {
return
}
x = (x >> 1) ^ uint64((int64(x&1)<<63)>>63)
return
}
// DecodeZigzag32 reads a zigzag-encoded 32-bit integer
// from the Buffer.
// This is the format used for the sint32 protocol buffer type.
func (p *Buffer) DecodeZigzag32() (x uint64, err error) {
x, err = p.DecodeVarint()
if err != nil {
return
}
x = uint64((uint32(x) >> 1) ^ uint32((int32(x&1)<<31)>>31))
return
}
// These are not ValueDecoders: they produce an array of bytes or a string.
// bytes, embedded messages
// DecodeRawBytes reads a count-delimited byte buffer from the Buffer.
// This is the format used for the bytes protocol buffer
// type and for embedded messages.
func (p *Buffer) DecodeRawBytes(alloc bool) (buf []byte, err error) {
n, err := p.DecodeVarint()
if err != nil {
return nil, err
}
nb := int(n)
if nb < 0 {
return nil, fmt.Errorf("proto: bad byte length %d", nb)
}
end := p.index + nb
if end < p.index || end > len(p.buf) {
return nil, io.ErrUnexpectedEOF
}
if !alloc {
// todo: check if can get more uses of alloc=false
buf = p.buf[p.index:end]
p.index += nb
return
}
buf = make([]byte, nb)
copy(buf, p.buf[p.index:])
p.index += nb
return
}
// DecodeStringBytes reads an encoded string from the Buffer.
// This is the format used for the proto2 string type.
func (p *Buffer) DecodeStringBytes() (s string, err error) {
buf, err := p.DecodeRawBytes(false)
if err != nil {
return
}
return string(buf), nil
}
// Skip the next item in the buffer. Its wire type is decoded and presented as an argument.
// If the protocol buffer has extensions, and the field matches, add it as an extension.
// Otherwise, if the XXX_unrecognized field exists, append the skipped data there.
func (o *Buffer) skipAndSave(t reflect.Type, tag, wire int, base structPointer, unrecField field) error {
oi := o.index
err := o.skip(t, tag, wire)
if err != nil {
return err
}
if !unrecField.IsValid() {
return nil
}
ptr := structPointer_Bytes(base, unrecField)
// Add the skipped field to struct field
obuf := o.buf
o.buf = *ptr
o.EncodeVarint(uint64(tag<<3 | wire))
*ptr = append(o.buf, obuf[oi:o.index]...)
o.buf = obuf
return nil
}
// Skip the next item in the buffer. Its wire type is decoded and presented as an argument.
func (o *Buffer) skip(t reflect.Type, tag, wire int) error {
var u uint64
var err error
switch wire {
case WireVarint:
_, err = o.DecodeVarint()
case WireFixed64:
_, err = o.DecodeFixed64()
case WireBytes:
_, err = o.DecodeRawBytes(false)
case WireFixed32:
_, err = o.DecodeFixed32()
case WireStartGroup:
for {
u, err = o.DecodeVarint()
if err != nil {
break
}
fwire := int(u & 0x7)
if fwire == WireEndGroup {
break
}
ftag := int(u >> 3)
err = o.skip(t, ftag, fwire)
if err != nil {
break
}
}
default:
err = fmt.Errorf("proto: can't skip unknown wire type %d for %s", wire, t)
}
return err
}
// Unmarshaler is the interface representing objects that can
// unmarshal themselves. The method should reset the receiver before
// decoding starts. The argument points to data that may be
// overwritten, so implementations should not keep references to the
// buffer.
type Unmarshaler interface {
Unmarshal([]byte) error
}
// Unmarshal parses the protocol buffer representation in buf and places the
// decoded result in pb. If the struct underlying pb does not match
// the data in buf, the results can be unpredictable.
//
// Unmarshal resets pb before starting to unmarshal, so any
// existing data in pb is always removed. Use UnmarshalMerge
// to preserve and append to existing data.
func Unmarshal(buf []byte, pb Message) error {
pb.Reset()
return UnmarshalMerge(buf, pb)
}
// UnmarshalMerge parses the protocol buffer representation in buf and
// writes the decoded result to pb. If the struct underlying pb does not match
// the data in buf, the results can be unpredictable.
//
// UnmarshalMerge merges into existing data in pb.
// Most code should use Unmarshal instead.
func UnmarshalMerge(buf []byte, pb Message) error {
// If the object can unmarshal itself, let it.
if u, ok := pb.(Unmarshaler); ok {
return u.Unmarshal(buf)
}
return NewBuffer(buf).Unmarshal(pb)
}
// DecodeMessage reads a count-delimited message from the Buffer.
func (p *Buffer) DecodeMessage(pb Message) error {
enc, err := p.DecodeRawBytes(false)
if err != nil {
return err
}
return NewBuffer(enc).Unmarshal(pb)
}
// DecodeGroup reads a tag-delimited group from the Buffer.
func (p *Buffer) DecodeGroup(pb Message) error {
typ, base, err := getbase(pb)
if err != nil {
return err
}
return p.unmarshalType(typ.Elem(), GetProperties(typ.Elem()), true, base)
}
// Unmarshal parses the protocol buffer representation in the
// Buffer and places the decoded result in pb. If the struct
// underlying pb does not match the data in the buffer, the results can be
// unpredictable.
//
// Unlike proto.Unmarshal, this does not reset pb before starting to unmarshal.
func (p *Buffer) Unmarshal(pb Message) error {
// If the object can unmarshal itself, let it.
if u, ok := pb.(Unmarshaler); ok {
err := u.Unmarshal(p.buf[p.index:])
p.index = len(p.buf)
return err
}
typ, base, err := getbase(pb)
if err != nil {
return err
}
err = p.unmarshalType(typ.Elem(), GetProperties(typ.Elem()), false, base)
if collectStats {
stats.Decode++
}
return err
}
// unmarshalType does the work of unmarshaling a structure.
func (o *Buffer) unmarshalType(st reflect.Type, prop *StructProperties, is_group bool, base structPointer) error {
var state errorState
required, reqFields := prop.reqCount, uint64(0)
var err error
for err == nil && o.index < len(o.buf) {
oi := o.index
var u uint64
u, err = o.DecodeVarint()
if err != nil {
break
}
wire := int(u & 0x7)
if wire == WireEndGroup {
if is_group {
if required > 0 {
// Not enough information to determine the exact field.
// (See below.)
return &RequiredNotSetError{"{Unknown}"}
}
return nil // input is satisfied
}
return fmt.Errorf("proto: %s: wiretype end group for non-group", st)
}
tag := int(u >> 3)
if tag <= 0 {
return fmt.Errorf("proto: %s: illegal tag %d (wire type %d)", st, tag, wire)
}
fieldnum, ok := prop.decoderTags.get(tag)
if !ok {
// Maybe it's an extension?
if prop.extendable {
if e, _ := extendable(structPointer_Interface(base, st)); isExtensionField(e, int32(tag)) {
if err = o.skip(st, tag, wire); err == nil {
extmap := e.extensionsWrite()
ext := extmap[int32(tag)] // may be missing
ext.enc = append(ext.enc, o.buf[oi:o.index]...)
extmap[int32(tag)] = ext
}
continue
}
}
// Maybe it's a oneof?
if prop.oneofUnmarshaler != nil {
m := structPointer_Interface(base, st).(Message)
// First return value indicates whether tag is a oneof field.
ok, err = prop.oneofUnmarshaler(m, tag, wire, o)
if err == ErrInternalBadWireType {
// Map the error to something more descriptive.
// Do the formatting here to save generated code space.
err = fmt.Errorf("bad wiretype for oneof field in %T", m)
}
if ok {
continue
}
}
err = o.skipAndSave(st, tag, wire, base, prop.unrecField)
continue
}
p := prop.Prop[fieldnum]
if p.dec == nil {
fmt.Fprintf(os.Stderr, "proto: no protobuf decoder for %s.%s\n", st, st.Field(fieldnum).Name)
continue
}
dec := p.dec
if wire != WireStartGroup && wire != p.WireType {
if wire == WireBytes && p.packedDec != nil {
// a packable field
dec = p.packedDec
} else {
err = fmt.Errorf("proto: bad wiretype for field %s.%s: got wiretype %d, want %d", st, st.Field(fieldnum).Name, wire, p.WireType)
continue
}
}
decErr := dec(o, p, base)
if decErr != nil && !state.shouldContinue(decErr, p) {
err = decErr
}
if err == nil && p.Required {
// Successfully decoded a required field.
if tag <= 64 {
// use bitmap for fields 1-64 to catch field reuse.
var mask uint64 = 1 << uint64(tag-1)
if reqFields&mask == 0 {
// new required field
reqFields |= mask
required--
}
} else {
// This is imprecise. It can be fooled by a required field
// with a tag > 64 that is encoded twice; that's very rare.
// A fully correct implementation would require allocating
// a data structure, which we would like to avoid.
required--
}
}
}
if err == nil {
if is_group {
return io.ErrUnexpectedEOF
}
if state.err != nil {
return state.err
}
if required > 0 {
// Not enough information to determine the exact field. If we use extra
// CPU, we could determine the field only if the missing required field
// has a tag <= 64 and we check reqFields.
return &RequiredNotSetError{"{Unknown}"}
}
}
return err
}
// Individual type decoders
// For each,
// u is the decoded value,
// v is a pointer to the field (pointer) in the struct
// Sizes of the pools to allocate inside the Buffer.
// The goal is modest amortization and allocation
// on at least 16-byte boundaries.
const (
boolPoolSize = 16
uint32PoolSize = 8
uint64PoolSize = 4
)
// Decode a bool.
func (o *Buffer) dec_bool(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
if len(o.bools) == 0 {
o.bools = make([]bool, boolPoolSize)
}
o.bools[0] = u != 0
*structPointer_Bool(base, p.field) = &o.bools[0]
o.bools = o.bools[1:]
return nil
}
func (o *Buffer) dec_proto3_bool(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
*structPointer_BoolVal(base, p.field) = u != 0
return nil
}
// Decode an int32.
func (o *Buffer) dec_int32(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
word32_Set(structPointer_Word32(base, p.field), o, uint32(u))
return nil
}
func (o *Buffer) dec_proto3_int32(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
word32Val_Set(structPointer_Word32Val(base, p.field), uint32(u))
return nil
}
// Decode an int64.
func (o *Buffer) dec_int64(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
word64_Set(structPointer_Word64(base, p.field), o, u)
return nil
}
func (o *Buffer) dec_proto3_int64(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
word64Val_Set(structPointer_Word64Val(base, p.field), o, u)
return nil
}
// Decode a string.
func (o *Buffer) dec_string(p *Properties, base structPointer) error {
s, err := o.DecodeStringBytes()
if err != nil {
return err
}
*structPointer_String(base, p.field) = &s
return nil
}
func (o *Buffer) dec_proto3_string(p *Properties, base structPointer) error {
s, err := o.DecodeStringBytes()
if err != nil {
return err
}
*structPointer_StringVal(base, p.field) = s
return nil
}
// Decode a slice of bytes ([]byte).
func (o *Buffer) dec_slice_byte(p *Properties, base structPointer) error {
b, err := o.DecodeRawBytes(true)
if err != nil {
return err
}
*structPointer_Bytes(base, p.field) = b
return nil
}
// Decode a slice of bools ([]bool).
func (o *Buffer) dec_slice_bool(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
v := structPointer_BoolSlice(base, p.field)
*v = append(*v, u != 0)
return nil
}
// Decode a slice of bools ([]bool) in packed format.
func (o *Buffer) dec_slice_packed_bool(p *Properties, base structPointer) error {
v := structPointer_BoolSlice(base, p.field)
nn, err := o.DecodeVarint()
if err != nil {
return err
}
nb := int(nn) // number of bytes of encoded bools
fin := o.index + nb
if fin < o.index {
return errOverflow
}
y := *v
for o.index < fin {
u, err := p.valDec(o)
if err != nil {
return err
}
y = append(y, u != 0)
}
*v = y
return nil
}
// Decode a slice of int32s ([]int32).
func (o *Buffer) dec_slice_int32(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
structPointer_Word32Slice(base, p.field).Append(uint32(u))
return nil
}
// Decode a slice of int32s ([]int32) in packed format.
func (o *Buffer) dec_slice_packed_int32(p *Properties, base structPointer) error {
v := structPointer_Word32Slice(base, p.field)
nn, err := o.DecodeVarint()
if err != nil {
return err
}
nb := int(nn) // number of bytes of encoded int32s
fin := o.index + nb
if fin < o.index {
return errOverflow
}
for o.index < fin {
u, err := p.valDec(o)
if err != nil {
return err
}
v.Append(uint32(u))
}
return nil
}
// Decode a slice of int64s ([]int64).
func (o *Buffer) dec_slice_int64(p *Properties, base structPointer) error {
u, err := p.valDec(o)
if err != nil {
return err
}
structPointer_Word64Slice(base, p.field).Append(u)
return nil
}
// Decode a slice of int64s ([]int64) in packed format.
func (o *Buffer) dec_slice_packed_int64(p *Properties, base structPointer) error {
v := structPointer_Word64Slice(base, p.field)
nn, err := o.DecodeVarint()
if err != nil {
return err
}
nb := int(nn) // number of bytes of encoded int64s
fin := o.index + nb
if fin < o.index {
return errOverflow
}
for o.index < fin {
u, err := p.valDec(o)
if err != nil {
return err
}
v.Append(u)
}
return nil
}
// Decode a slice of strings ([]string).
func (o *Buffer) dec_slice_string(p *Properties, base structPointer) error {
s, err := o.DecodeStringBytes()
if err != nil {
return err
}
v := structPointer_StringSlice(base, p.field)
*v = append(*v, s)
return nil
}
// Decode a slice of slice of bytes ([][]byte).
func (o *Buffer) dec_slice_slice_byte(p *Properties, base structPointer) error {
b, err := o.DecodeRawBytes(true)
if err != nil {
return err
}
v := structPointer_BytesSlice(base, p.field)
*v = append(*v, b)
return nil
}
// Decode a map field.
func (o *Buffer) dec_new_map(p *Properties, base structPointer) error {
raw, err := o.DecodeRawBytes(false)
if err != nil {
return err
}
oi := o.index // index at the end of this map entry
o.index -= len(raw) // move buffer back to start of map entry
mptr := structPointer_NewAt(base, p.field, p.mtype) // *map[K]V
if mptr.Elem().IsNil() {
mptr.Elem().Set(reflect.MakeMap(mptr.Type().Elem()))
}
v := mptr.Elem() // map[K]V
// Prepare addressable doubly-indirect placeholders for the key and value types.
// See enc_new_map for why.
keyptr := reflect.New(reflect.PtrTo(p.mtype.Key())).Elem() // addressable *K
keybase := toStructPointer(keyptr.Addr()) // **K
var valbase structPointer
var valptr reflect.Value
switch p.mtype.Elem().Kind() {
case reflect.Slice:
// []byte
var dummy []byte
valptr = reflect.ValueOf(&dummy) // *[]byte
valbase = toStructPointer(valptr) // *[]byte
case reflect.Ptr:
// message; valptr is **Msg; need to allocate the intermediate pointer
valptr = reflect.New(reflect.PtrTo(p.mtype.Elem())).Elem() // addressable *V
valptr.Set(reflect.New(valptr.Type().Elem()))
valbase = toStructPointer(valptr)
default:
// everything else
valptr = reflect.New(reflect.PtrTo(p.mtype.Elem())).Elem() // addressable *V
valbase = toStructPointer(valptr.Addr()) // **V
}
// Decode.
// This parses a restricted wire format, namely the encoding of a message
// with two fields. See enc_new_map for the format.
for o.index < oi {
// tagcode for key and value properties are always a single byte
// because they have tags 1 and 2.
tagcode := o.buf[o.index]
o.index++
switch tagcode {
case p.mkeyprop.tagcode[0]:
if err := p.mkeyprop.dec(o, p.mkeyprop, keybase); err != nil {
return err
}
case p.mvalprop.tagcode[0]:
if err := p.mvalprop.dec(o, p.mvalprop, valbase); err != nil {
return err
}
default:
// TODO: Should we silently skip this instead?
return fmt.Errorf("proto: bad map data tag %d", raw[0])
}
}
keyelem, valelem := keyptr.Elem(), valptr.Elem()
if !keyelem.IsValid() {
keyelem = reflect.Zero(p.mtype.Key())
}
if !valelem.IsValid() {
valelem = reflect.Zero(p.mtype.Elem())
}
v.SetMapIndex(keyelem, valelem)
return nil
}
// Decode a group.
func (o *Buffer) dec_struct_group(p *Properties, base structPointer) error {
bas := structPointer_GetStructPointer(base, p.field)
if structPointer_IsNil(bas) {
// allocate new nested message
bas = toStructPointer(reflect.New(p.stype))
structPointer_SetStructPointer(base, p.field, bas)
}
return o.unmarshalType(p.stype, p.sprop, true, bas)
}
// Decode an embedded message.
func (o *Buffer) dec_struct_message(p *Properties, base structPointer) (err error) {
raw, e := o.DecodeRawBytes(false)
if e != nil {
return e
}
bas := structPointer_GetStructPointer(base, p.field)
if structPointer_IsNil(bas) {
// allocate new nested message
bas = toStructPointer(reflect.New(p.stype))
structPointer_SetStructPointer(base, p.field, bas)
}
// If the object can unmarshal itself, let it.
if p.isUnmarshaler {
iv := structPointer_Interface(bas, p.stype)
return iv.(Unmarshaler).Unmarshal(raw)
}
obuf := o.buf
oi := o.index
o.buf = raw
o.index = 0
err = o.unmarshalType(p.stype, p.sprop, false, bas)
o.buf = obuf
o.index = oi
return err
}
// Decode a slice of embedded messages.
func (o *Buffer) dec_slice_struct_message(p *Properties, base structPointer) error {
return o.dec_slice_struct(p, false, base)
}
// Decode a slice of embedded groups.
func (o *Buffer) dec_slice_struct_group(p *Properties, base structPointer) error {
return o.dec_slice_struct(p, true, base)
}
// Decode a slice of structs ([]*struct).
func (o *Buffer) dec_slice_struct(p *Properties, is_group bool, base structPointer) error {
v := reflect.New(p.stype)
bas := toStructPointer(v)
structPointer_StructPointerSlice(base, p.field).Append(bas)
if is_group {
err := o.unmarshalType(p.stype, p.sprop, is_group, bas)
return err
}
raw, err := o.DecodeRawBytes(false)
if err != nil {
return err
}
// If the object can unmarshal itself, let it.
if p.isUnmarshaler {
iv := v.Interface()
return iv.(Unmarshaler).Unmarshal(raw)
}
obuf := o.buf
oi := o.index
o.buf = raw
o.index = 0
err = o.unmarshalType(p.stype, p.sprop, is_group, bas)
o.buf = obuf
o.index = oi
return err
}
+1362
View File
File diff suppressed because it is too large Load Diff
+300
View File
@@ -0,0 +1,300 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2011 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Protocol buffer comparison.
package proto
import (
"bytes"
"log"
"reflect"
"strings"
)
/*
Equal returns true iff protocol buffers a and b are equal.
The arguments must both be pointers to protocol buffer structs.
Equality is defined in this way:
- Two messages are equal iff they are the same type,
corresponding fields are equal, unknown field sets
are equal, and extensions sets are equal.
- Two set scalar fields are equal iff their values are equal.
If the fields are of a floating-point type, remember that
NaN != x for all x, including NaN. If the message is defined
in a proto3 .proto file, fields are not "set"; specifically,
zero length proto3 "bytes" fields are equal (nil == {}).
- Two repeated fields are equal iff their lengths are the same,
and their corresponding elements are equal. Note a "bytes" field,
although represented by []byte, is not a repeated field and the
rule for the scalar fields described above applies.
- Two unset fields are equal.
- Two unknown field sets are equal if their current
encoded state is equal.
- Two extension sets are equal iff they have corresponding
elements that are pairwise equal.
- Two map fields are equal iff their lengths are the same,
and they contain the same set of elements. Zero-length map
fields are equal.
- Every other combination of things are not equal.
The return value is undefined if a and b are not protocol buffers.
*/
func Equal(a, b Message) bool {
if a == nil || b == nil {
return a == b
}
v1, v2 := reflect.ValueOf(a), reflect.ValueOf(b)
if v1.Type() != v2.Type() {
return false
}
if v1.Kind() == reflect.Ptr {
if v1.IsNil() {
return v2.IsNil()
}
if v2.IsNil() {
return false
}
v1, v2 = v1.Elem(), v2.Elem()
}
if v1.Kind() != reflect.Struct {
return false
}
return equalStruct(v1, v2)
}
// v1 and v2 are known to have the same type.
func equalStruct(v1, v2 reflect.Value) bool {
sprop := GetProperties(v1.Type())
for i := 0; i < v1.NumField(); i++ {
f := v1.Type().Field(i)
if strings.HasPrefix(f.Name, "XXX_") {
continue
}
f1, f2 := v1.Field(i), v2.Field(i)
if f.Type.Kind() == reflect.Ptr {
if n1, n2 := f1.IsNil(), f2.IsNil(); n1 && n2 {
// both unset
continue
} else if n1 != n2 {
// set/unset mismatch
return false
}
b1, ok := f1.Interface().(raw)
if ok {
b2 := f2.Interface().(raw)
// RawMessage
if !bytes.Equal(b1.Bytes(), b2.Bytes()) {
return false
}
continue
}
f1, f2 = f1.Elem(), f2.Elem()
}
if !equalAny(f1, f2, sprop.Prop[i]) {
return false
}
}
if em1 := v1.FieldByName("XXX_InternalExtensions"); em1.IsValid() {
em2 := v2.FieldByName("XXX_InternalExtensions")
if !equalExtensions(v1.Type(), em1.Interface().(XXX_InternalExtensions), em2.Interface().(XXX_InternalExtensions)) {
return false
}
}
if em1 := v1.FieldByName("XXX_extensions"); em1.IsValid() {
em2 := v2.FieldByName("XXX_extensions")
if !equalExtMap(v1.Type(), em1.Interface().(map[int32]Extension), em2.Interface().(map[int32]Extension)) {
return false
}
}
uf := v1.FieldByName("XXX_unrecognized")
if !uf.IsValid() {
return true
}
u1 := uf.Bytes()
u2 := v2.FieldByName("XXX_unrecognized").Bytes()
if !bytes.Equal(u1, u2) {
return false
}
return true
}
// v1 and v2 are known to have the same type.
// prop may be nil.
func equalAny(v1, v2 reflect.Value, prop *Properties) bool {
if v1.Type() == protoMessageType {
m1, _ := v1.Interface().(Message)
m2, _ := v2.Interface().(Message)
return Equal(m1, m2)
}
switch v1.Kind() {
case reflect.Bool:
return v1.Bool() == v2.Bool()
case reflect.Float32, reflect.Float64:
return v1.Float() == v2.Float()
case reflect.Int32, reflect.Int64:
return v1.Int() == v2.Int()
case reflect.Interface:
// Probably a oneof field; compare the inner values.
n1, n2 := v1.IsNil(), v2.IsNil()
if n1 || n2 {
return n1 == n2
}
e1, e2 := v1.Elem(), v2.Elem()
if e1.Type() != e2.Type() {
return false
}
return equalAny(e1, e2, nil)
case reflect.Map:
if v1.Len() != v2.Len() {
return false
}
for _, key := range v1.MapKeys() {
val2 := v2.MapIndex(key)
if !val2.IsValid() {
// This key was not found in the second map.
return false
}
if !equalAny(v1.MapIndex(key), val2, nil) {
return false
}
}
return true
case reflect.Ptr:
// Maps may have nil values in them, so check for nil.
if v1.IsNil() && v2.IsNil() {
return true
}
if v1.IsNil() != v2.IsNil() {
return false
}
return equalAny(v1.Elem(), v2.Elem(), prop)
case reflect.Slice:
if v1.Type().Elem().Kind() == reflect.Uint8 {
// short circuit: []byte
// Edge case: if this is in a proto3 message, a zero length
// bytes field is considered the zero value.
if prop != nil && prop.proto3 && v1.Len() == 0 && v2.Len() == 0 {
return true
}
if v1.IsNil() != v2.IsNil() {
return false
}
return bytes.Equal(v1.Interface().([]byte), v2.Interface().([]byte))
}
if v1.Len() != v2.Len() {
return false
}
for i := 0; i < v1.Len(); i++ {
if !equalAny(v1.Index(i), v2.Index(i), prop) {
return false
}
}
return true
case reflect.String:
return v1.Interface().(string) == v2.Interface().(string)
case reflect.Struct:
return equalStruct(v1, v2)
case reflect.Uint32, reflect.Uint64:
return v1.Uint() == v2.Uint()
}
// unknown type, so not a protocol buffer
log.Printf("proto: don't know how to compare %v", v1)
return false
}
// base is the struct type that the extensions are based on.
// x1 and x2 are InternalExtensions.
func equalExtensions(base reflect.Type, x1, x2 XXX_InternalExtensions) bool {
em1, _ := x1.extensionsRead()
em2, _ := x2.extensionsRead()
return equalExtMap(base, em1, em2)
}
func equalExtMap(base reflect.Type, em1, em2 map[int32]Extension) bool {
if len(em1) != len(em2) {
return false
}
for extNum, e1 := range em1 {
e2, ok := em2[extNum]
if !ok {
return false
}
m1, m2 := e1.value, e2.value
if m1 != nil && m2 != nil {
// Both are unencoded.
if !equalAny(reflect.ValueOf(m1), reflect.ValueOf(m2), nil) {
return false
}
continue
}
// At least one is encoded. To do a semantically correct comparison
// we need to unmarshal them first.
var desc *ExtensionDesc
if m := extensionMaps[base]; m != nil {
desc = m[extNum]
}
if desc == nil {
log.Printf("proto: don't know how to compare extension %d of %v", extNum, base)
continue
}
var err error
if m1 == nil {
m1, err = decodeExtension(e1.enc, desc)
}
if m2 == nil && err == nil {
m2, err = decodeExtension(e2.enc, desc)
}
if err != nil {
// The encoded form is invalid.
log.Printf("proto: badly encoded extension %d of %v: %v", extNum, base, err)
return false
}
if !equalAny(reflect.ValueOf(m1), reflect.ValueOf(m2), nil) {
return false
}
}
return true
}
+587
View File
@@ -0,0 +1,587 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2010 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package proto
/*
* Types and routines for supporting protocol buffer extensions.
*/
import (
"errors"
"fmt"
"reflect"
"strconv"
"sync"
)
// ErrMissingExtension is the error returned by GetExtension if the named extension is not in the message.
var ErrMissingExtension = errors.New("proto: missing extension")
// ExtensionRange represents a range of message extensions for a protocol buffer.
// Used in code generated by the protocol compiler.
type ExtensionRange struct {
Start, End int32 // both inclusive
}
// extendableProto is an interface implemented by any protocol buffer generated by the current
// proto compiler that may be extended.
type extendableProto interface {
Message
ExtensionRangeArray() []ExtensionRange
extensionsWrite() map[int32]Extension
extensionsRead() (map[int32]Extension, sync.Locker)
}
// extendableProtoV1 is an interface implemented by a protocol buffer generated by the previous
// version of the proto compiler that may be extended.
type extendableProtoV1 interface {
Message
ExtensionRangeArray() []ExtensionRange
ExtensionMap() map[int32]Extension
}
// extensionAdapter is a wrapper around extendableProtoV1 that implements extendableProto.
type extensionAdapter struct {
extendableProtoV1
}
func (e extensionAdapter) extensionsWrite() map[int32]Extension {
return e.ExtensionMap()
}
func (e extensionAdapter) extensionsRead() (map[int32]Extension, sync.Locker) {
return e.ExtensionMap(), notLocker{}
}
// notLocker is a sync.Locker whose Lock and Unlock methods are nops.
type notLocker struct{}
func (n notLocker) Lock() {}
func (n notLocker) Unlock() {}
// extendable returns the extendableProto interface for the given generated proto message.
// If the proto message has the old extension format, it returns a wrapper that implements
// the extendableProto interface.
func extendable(p interface{}) (extendableProto, bool) {
if ep, ok := p.(extendableProto); ok {
return ep, ok
}
if ep, ok := p.(extendableProtoV1); ok {
return extensionAdapter{ep}, ok
}
return nil, false
}
// XXX_InternalExtensions is an internal representation of proto extensions.
//
// Each generated message struct type embeds an anonymous XXX_InternalExtensions field,
// thus gaining the unexported 'extensions' method, which can be called only from the proto package.
//
// The methods of XXX_InternalExtensions are not concurrency safe in general,
// but calls to logically read-only methods such as has and get may be executed concurrently.
type XXX_InternalExtensions struct {
// The struct must be indirect so that if a user inadvertently copies a
// generated message and its embedded XXX_InternalExtensions, they
// avoid the mayhem of a copied mutex.
//
// The mutex serializes all logically read-only operations to p.extensionMap.
// It is up to the client to ensure that write operations to p.extensionMap are
// mutually exclusive with other accesses.
p *struct {
mu sync.Mutex
extensionMap map[int32]Extension
}
}
// extensionsWrite returns the extension map, creating it on first use.
func (e *XXX_InternalExtensions) extensionsWrite() map[int32]Extension {
if e.p == nil {
e.p = new(struct {
mu sync.Mutex
extensionMap map[int32]Extension
})
e.p.extensionMap = make(map[int32]Extension)
}
return e.p.extensionMap
}
// extensionsRead returns the extensions map for read-only use. It may be nil.
// The caller must hold the returned mutex's lock when accessing Elements within the map.
func (e *XXX_InternalExtensions) extensionsRead() (map[int32]Extension, sync.Locker) {
if e.p == nil {
return nil, nil
}
return e.p.extensionMap, &e.p.mu
}
var extendableProtoType = reflect.TypeOf((*extendableProto)(nil)).Elem()
var extendableProtoV1Type = reflect.TypeOf((*extendableProtoV1)(nil)).Elem()
// ExtensionDesc represents an extension specification.
// Used in generated code from the protocol compiler.
type ExtensionDesc struct {
ExtendedType Message // nil pointer to the type that is being extended
ExtensionType interface{} // nil pointer to the extension type
Field int32 // field number
Name string // fully-qualified name of extension, for text formatting
Tag string // protobuf tag style
Filename string // name of the file in which the extension is defined
}
func (ed *ExtensionDesc) repeated() bool {
t := reflect.TypeOf(ed.ExtensionType)
return t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8
}
// Extension represents an extension in a message.
type Extension struct {
// When an extension is stored in a message using SetExtension
// only desc and value are set. When the message is marshaled
// enc will be set to the encoded form of the message.
//
// When a message is unmarshaled and contains extensions, each
// extension will have only enc set. When such an extension is
// accessed using GetExtension (or GetExtensions) desc and value
// will be set.
desc *ExtensionDesc
value interface{}
enc []byte
}
// SetRawExtension is for testing only.
func SetRawExtension(base Message, id int32, b []byte) {
epb, ok := extendable(base)
if !ok {
return
}
extmap := epb.extensionsWrite()
extmap[id] = Extension{enc: b}
}
// isExtensionField returns true iff the given field number is in an extension range.
func isExtensionField(pb extendableProto, field int32) bool {
for _, er := range pb.ExtensionRangeArray() {
if er.Start <= field && field <= er.End {
return true
}
}
return false
}
// checkExtensionTypes checks that the given extension is valid for pb.
func checkExtensionTypes(pb extendableProto, extension *ExtensionDesc) error {
var pbi interface{} = pb
// Check the extended type.
if ea, ok := pbi.(extensionAdapter); ok {
pbi = ea.extendableProtoV1
}
if a, b := reflect.TypeOf(pbi), reflect.TypeOf(extension.ExtendedType); a != b {
return errors.New("proto: bad extended type; " + b.String() + " does not extend " + a.String())
}
// Check the range.
if !isExtensionField(pb, extension.Field) {
return errors.New("proto: bad extension number; not in declared ranges")
}
return nil
}
// extPropKey is sufficient to uniquely identify an extension.
type extPropKey struct {
base reflect.Type
field int32
}
var extProp = struct {
sync.RWMutex
m map[extPropKey]*Properties
}{
m: make(map[extPropKey]*Properties),
}
func extensionProperties(ed *ExtensionDesc) *Properties {
key := extPropKey{base: reflect.TypeOf(ed.ExtendedType), field: ed.Field}
extProp.RLock()
if prop, ok := extProp.m[key]; ok {
extProp.RUnlock()
return prop
}
extProp.RUnlock()
extProp.Lock()
defer extProp.Unlock()
// Check again.
if prop, ok := extProp.m[key]; ok {
return prop
}
prop := new(Properties)
prop.Init(reflect.TypeOf(ed.ExtensionType), "unknown_name", ed.Tag, nil)
extProp.m[key] = prop
return prop
}
// encode encodes any unmarshaled (unencoded) extensions in e.
func encodeExtensions(e *XXX_InternalExtensions) error {
m, mu := e.extensionsRead()
if m == nil {
return nil // fast path
}
mu.Lock()
defer mu.Unlock()
return encodeExtensionsMap(m)
}
// encode encodes any unmarshaled (unencoded) extensions in e.
func encodeExtensionsMap(m map[int32]Extension) error {
for k, e := range m {
if e.value == nil || e.desc == nil {
// Extension is only in its encoded form.
continue
}
// We don't skip extensions that have an encoded form set,
// because the extension value may have been mutated after
// the last time this function was called.
et := reflect.TypeOf(e.desc.ExtensionType)
props := extensionProperties(e.desc)
p := NewBuffer(nil)
// If e.value has type T, the encoder expects a *struct{ X T }.
// Pass a *T with a zero field and hope it all works out.
x := reflect.New(et)
x.Elem().Set(reflect.ValueOf(e.value))
if err := props.enc(p, props, toStructPointer(x)); err != nil {
return err
}
e.enc = p.buf
m[k] = e
}
return nil
}
func extensionsSize(e *XXX_InternalExtensions) (n int) {
m, mu := e.extensionsRead()
if m == nil {
return 0
}
mu.Lock()
defer mu.Unlock()
return extensionsMapSize(m)
}
func extensionsMapSize(m map[int32]Extension) (n int) {
for _, e := range m {
if e.value == nil || e.desc == nil {
// Extension is only in its encoded form.
n += len(e.enc)
continue
}
// We don't skip extensions that have an encoded form set,
// because the extension value may have been mutated after
// the last time this function was called.
et := reflect.TypeOf(e.desc.ExtensionType)
props := extensionProperties(e.desc)
// If e.value has type T, the encoder expects a *struct{ X T }.
// Pass a *T with a zero field and hope it all works out.
x := reflect.New(et)
x.Elem().Set(reflect.ValueOf(e.value))
n += props.size(props, toStructPointer(x))
}
return
}
// HasExtension returns whether the given extension is present in pb.
func HasExtension(pb Message, extension *ExtensionDesc) bool {
// TODO: Check types, field numbers, etc.?
epb, ok := extendable(pb)
if !ok {
return false
}
extmap, mu := epb.extensionsRead()
if extmap == nil {
return false
}
mu.Lock()
_, ok = extmap[extension.Field]
mu.Unlock()
return ok
}
// ClearExtension removes the given extension from pb.
func ClearExtension(pb Message, extension *ExtensionDesc) {
epb, ok := extendable(pb)
if !ok {
return
}
// TODO: Check types, field numbers, etc.?
extmap := epb.extensionsWrite()
delete(extmap, extension.Field)
}
// GetExtension parses and returns the given extension of pb.
// If the extension is not present and has no default value it returns ErrMissingExtension.
func GetExtension(pb Message, extension *ExtensionDesc) (interface{}, error) {
epb, ok := extendable(pb)
if !ok {
return nil, errors.New("proto: not an extendable proto")
}
if err := checkExtensionTypes(epb, extension); err != nil {
return nil, err
}
emap, mu := epb.extensionsRead()
if emap == nil {
return defaultExtensionValue(extension)
}
mu.Lock()
defer mu.Unlock()
e, ok := emap[extension.Field]
if !ok {
// defaultExtensionValue returns the default value or
// ErrMissingExtension if there is no default.
return defaultExtensionValue(extension)
}
if e.value != nil {
// Already decoded. Check the descriptor, though.
if e.desc != extension {
// This shouldn't happen. If it does, it means that
// GetExtension was called twice with two different
// descriptors with the same field number.
return nil, errors.New("proto: descriptor conflict")
}
return e.value, nil
}
v, err := decodeExtension(e.enc, extension)
if err != nil {
return nil, err
}
// Remember the decoded version and drop the encoded version.
// That way it is safe to mutate what we return.
e.value = v
e.desc = extension
e.enc = nil
emap[extension.Field] = e
return e.value, nil
}
// defaultExtensionValue returns the default value for extension.
// If no default for an extension is defined ErrMissingExtension is returned.
func defaultExtensionValue(extension *ExtensionDesc) (interface{}, error) {
t := reflect.TypeOf(extension.ExtensionType)
props := extensionProperties(extension)
sf, _, err := fieldDefault(t, props)
if err != nil {
return nil, err
}
if sf == nil || sf.value == nil {
// There is no default value.
return nil, ErrMissingExtension
}
if t.Kind() != reflect.Ptr {
// We do not need to return a Ptr, we can directly return sf.value.
return sf.value, nil
}
// We need to return an interface{} that is a pointer to sf.value.
value := reflect.New(t).Elem()
value.Set(reflect.New(value.Type().Elem()))
if sf.kind == reflect.Int32 {
// We may have an int32 or an enum, but the underlying data is int32.
// Since we can't set an int32 into a non int32 reflect.value directly
// set it as a int32.
value.Elem().SetInt(int64(sf.value.(int32)))
} else {
value.Elem().Set(reflect.ValueOf(sf.value))
}
return value.Interface(), nil
}
// decodeExtension decodes an extension encoded in b.
func decodeExtension(b []byte, extension *ExtensionDesc) (interface{}, error) {
o := NewBuffer(b)
t := reflect.TypeOf(extension.ExtensionType)
props := extensionProperties(extension)
// t is a pointer to a struct, pointer to basic type or a slice.
// Allocate a "field" to store the pointer/slice itself; the
// pointer/slice will be stored here. We pass
// the address of this field to props.dec.
// This passes a zero field and a *t and lets props.dec
// interpret it as a *struct{ x t }.
value := reflect.New(t).Elem()
for {
// Discard wire type and field number varint. It isn't needed.
if _, err := o.DecodeVarint(); err != nil {
return nil, err
}
if err := props.dec(o, props, toStructPointer(value.Addr())); err != nil {
return nil, err
}
if o.index >= len(o.buf) {
break
}
}
return value.Interface(), nil
}
// GetExtensions returns a slice of the extensions present in pb that are also listed in es.
// The returned slice has the same length as es; missing extensions will appear as nil elements.
func GetExtensions(pb Message, es []*ExtensionDesc) (extensions []interface{}, err error) {
epb, ok := extendable(pb)
if !ok {
return nil, errors.New("proto: not an extendable proto")
}
extensions = make([]interface{}, len(es))
for i, e := range es {
extensions[i], err = GetExtension(epb, e)
if err == ErrMissingExtension {
err = nil
}
if err != nil {
return
}
}
return
}
// ExtensionDescs returns a new slice containing pb's extension descriptors, in undefined order.
// For non-registered extensions, ExtensionDescs returns an incomplete descriptor containing
// just the Field field, which defines the extension's field number.
func ExtensionDescs(pb Message) ([]*ExtensionDesc, error) {
epb, ok := extendable(pb)
if !ok {
return nil, fmt.Errorf("proto: %T is not an extendable proto.Message", pb)
}
registeredExtensions := RegisteredExtensions(pb)
emap, mu := epb.extensionsRead()
if emap == nil {
return nil, nil
}
mu.Lock()
defer mu.Unlock()
extensions := make([]*ExtensionDesc, 0, len(emap))
for extid, e := range emap {
desc := e.desc
if desc == nil {
desc = registeredExtensions[extid]
if desc == nil {
desc = &ExtensionDesc{Field: extid}
}
}
extensions = append(extensions, desc)
}
return extensions, nil
}
// SetExtension sets the specified extension of pb to the specified value.
func SetExtension(pb Message, extension *ExtensionDesc, value interface{}) error {
epb, ok := extendable(pb)
if !ok {
return errors.New("proto: not an extendable proto")
}
if err := checkExtensionTypes(epb, extension); err != nil {
return err
}
typ := reflect.TypeOf(extension.ExtensionType)
if typ != reflect.TypeOf(value) {
return errors.New("proto: bad extension value type")
}
// nil extension values need to be caught early, because the
// encoder can't distinguish an ErrNil due to a nil extension
// from an ErrNil due to a missing field. Extensions are
// always optional, so the encoder would just swallow the error
// and drop all the extensions from the encoded message.
if reflect.ValueOf(value).IsNil() {
return fmt.Errorf("proto: SetExtension called with nil value of type %T", value)
}
extmap := epb.extensionsWrite()
extmap[extension.Field] = Extension{desc: extension, value: value}
return nil
}
// ClearAllExtensions clears all extensions from pb.
func ClearAllExtensions(pb Message) {
epb, ok := extendable(pb)
if !ok {
return
}
m := epb.extensionsWrite()
for k := range m {
delete(m, k)
}
}
// A global registry of extensions.
// The generated code will register the generated descriptors by calling RegisterExtension.
var extensionMaps = make(map[reflect.Type]map[int32]*ExtensionDesc)
// RegisterExtension is called from the generated code.
func RegisterExtension(desc *ExtensionDesc) {
st := reflect.TypeOf(desc.ExtendedType).Elem()
m := extensionMaps[st]
if m == nil {
m = make(map[int32]*ExtensionDesc)
extensionMaps[st] = m
}
if _, ok := m[desc.Field]; ok {
panic("proto: duplicate extension registered: " + st.String() + " " + strconv.Itoa(int(desc.Field)))
}
m[desc.Field] = desc
}
// RegisteredExtensions returns a map of the registered extensions of a
// protocol buffer struct, indexed by the extension number.
// The argument pb should be a nil pointer to the struct type.
func RegisteredExtensions(pb Message) map[int32]*ExtensionDesc {
return extensionMaps[reflect.TypeOf(pb).Elem()]
}
+898
View File
@@ -0,0 +1,898 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2010 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/*
Package proto converts data structures to and from the wire format of
protocol buffers. It works in concert with the Go source code generated
for .proto files by the protocol compiler.
A summary of the properties of the protocol buffer interface
for a protocol buffer variable v:
- Names are turned from camel_case to CamelCase for export.
- There are no methods on v to set fields; just treat
them as structure fields.
- There are getters that return a field's value if set,
and return the field's default value if unset.
The getters work even if the receiver is a nil message.
- The zero value for a struct is its correct initialization state.
All desired fields must be set before marshaling.
- A Reset() method will restore a protobuf struct to its zero state.
- Non-repeated fields are pointers to the values; nil means unset.
That is, optional or required field int32 f becomes F *int32.
- Repeated fields are slices.
- Helper functions are available to aid the setting of fields.
msg.Foo = proto.String("hello") // set field
- Constants are defined to hold the default values of all fields that
have them. They have the form Default_StructName_FieldName.
Because the getter methods handle defaulted values,
direct use of these constants should be rare.
- Enums are given type names and maps from names to values.
Enum values are prefixed by the enclosing message's name, or by the
enum's type name if it is a top-level enum. Enum types have a String
method, and a Enum method to assist in message construction.
- Nested messages, groups and enums have type names prefixed with the name of
the surrounding message type.
- Extensions are given descriptor names that start with E_,
followed by an underscore-delimited list of the nested messages
that contain it (if any) followed by the CamelCased name of the
extension field itself. HasExtension, ClearExtension, GetExtension
and SetExtension are functions for manipulating extensions.
- Oneof field sets are given a single field in their message,
with distinguished wrapper types for each possible field value.
- Marshal and Unmarshal are functions to encode and decode the wire format.
When the .proto file specifies `syntax="proto3"`, there are some differences:
- Non-repeated fields of non-message type are values instead of pointers.
- Getters are only generated for message and oneof fields.
- Enum types do not get an Enum method.
The simplest way to describe this is to see an example.
Given file test.proto, containing
package example;
enum FOO { X = 17; }
message Test {
required string label = 1;
optional int32 type = 2 [default=77];
repeated int64 reps = 3;
optional group OptionalGroup = 4 {
required string RequiredField = 5;
}
oneof union {
int32 number = 6;
string name = 7;
}
}
The resulting file, test.pb.go, is:
package example
import proto "github.com/golang/protobuf/proto"
import math "math"
type FOO int32
const (
FOO_X FOO = 17
)
var FOO_name = map[int32]string{
17: "X",
}
var FOO_value = map[string]int32{
"X": 17,
}
func (x FOO) Enum() *FOO {
p := new(FOO)
*p = x
return p
}
func (x FOO) String() string {
return proto.EnumName(FOO_name, int32(x))
}
func (x *FOO) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(FOO_value, data)
if err != nil {
return err
}
*x = FOO(value)
return nil
}
type Test struct {
Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
// Types that are valid to be assigned to Union:
// *Test_Number
// *Test_Name
Union isTest_Union `protobuf_oneof:"union"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Test) Reset() { *m = Test{} }
func (m *Test) String() string { return proto.CompactTextString(m) }
func (*Test) ProtoMessage() {}
type isTest_Union interface {
isTest_Union()
}
type Test_Number struct {
Number int32 `protobuf:"varint,6,opt,name=number"`
}
type Test_Name struct {
Name string `protobuf:"bytes,7,opt,name=name"`
}
func (*Test_Number) isTest_Union() {}
func (*Test_Name) isTest_Union() {}
func (m *Test) GetUnion() isTest_Union {
if m != nil {
return m.Union
}
return nil
}
const Default_Test_Type int32 = 77
func (m *Test) GetLabel() string {
if m != nil && m.Label != nil {
return *m.Label
}
return ""
}
func (m *Test) GetType() int32 {
if m != nil && m.Type != nil {
return *m.Type
}
return Default_Test_Type
}
func (m *Test) GetOptionalgroup() *Test_OptionalGroup {
if m != nil {
return m.Optionalgroup
}
return nil
}
type Test_OptionalGroup struct {
RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"`
}
func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} }
func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) }
func (m *Test_OptionalGroup) GetRequiredField() string {
if m != nil && m.RequiredField != nil {
return *m.RequiredField
}
return ""
}
func (m *Test) GetNumber() int32 {
if x, ok := m.GetUnion().(*Test_Number); ok {
return x.Number
}
return 0
}
func (m *Test) GetName() string {
if x, ok := m.GetUnion().(*Test_Name); ok {
return x.Name
}
return ""
}
func init() {
proto.RegisterEnum("example.FOO", FOO_name, FOO_value)
}
To create and play with a Test object:
package main
import (
"log"
"github.com/golang/protobuf/proto"
pb "./example.pb"
)
func main() {
test := &pb.Test{
Label: proto.String("hello"),
Type: proto.Int32(17),
Reps: []int64{1, 2, 3},
Optionalgroup: &pb.Test_OptionalGroup{
RequiredField: proto.String("good bye"),
},
Union: &pb.Test_Name{"fred"},
}
data, err := proto.Marshal(test)
if err != nil {
log.Fatal("marshaling error: ", err)
}
newTest := &pb.Test{}
err = proto.Unmarshal(data, newTest)
if err != nil {
log.Fatal("unmarshaling error: ", err)
}
// Now test and newTest contain the same data.
if test.GetLabel() != newTest.GetLabel() {
log.Fatalf("data mismatch %q != %q", test.GetLabel(), newTest.GetLabel())
}
// Use a type switch to determine which oneof was set.
switch u := test.Union.(type) {
case *pb.Test_Number: // u.Number contains the number.
case *pb.Test_Name: // u.Name contains the string.
}
// etc.
}
*/
package proto
import (
"encoding/json"
"fmt"
"log"
"reflect"
"sort"
"strconv"
"sync"
)
// Message is implemented by generated protocol buffer messages.
type Message interface {
Reset()
String() string
ProtoMessage()
}
// Stats records allocation details about the protocol buffer encoders
// and decoders. Useful for tuning the library itself.
type Stats struct {
Emalloc uint64 // mallocs in encode
Dmalloc uint64 // mallocs in decode
Encode uint64 // number of encodes
Decode uint64 // number of decodes
Chit uint64 // number of cache hits
Cmiss uint64 // number of cache misses
Size uint64 // number of sizes
}
// Set to true to enable stats collection.
const collectStats = false
var stats Stats
// GetStats returns a copy of the global Stats structure.
func GetStats() Stats { return stats }
// A Buffer is a buffer manager for marshaling and unmarshaling
// protocol buffers. It may be reused between invocations to
// reduce memory usage. It is not necessary to use a Buffer;
// the global functions Marshal and Unmarshal create a
// temporary Buffer and are fine for most applications.
type Buffer struct {
buf []byte // encode/decode byte stream
index int // read point
// pools of basic types to amortize allocation.
bools []bool
uint32s []uint32
uint64s []uint64
// extra pools, only used with pointer_reflect.go
int32s []int32
int64s []int64
float32s []float32
float64s []float64
}
// NewBuffer allocates a new Buffer and initializes its internal data to
// the contents of the argument slice.
func NewBuffer(e []byte) *Buffer {
return &Buffer{buf: e}
}
// Reset resets the Buffer, ready for marshaling a new protocol buffer.
func (p *Buffer) Reset() {
p.buf = p.buf[0:0] // for reading/writing
p.index = 0 // for reading
}
// SetBuf replaces the internal buffer with the slice,
// ready for unmarshaling the contents of the slice.
func (p *Buffer) SetBuf(s []byte) {
p.buf = s
p.index = 0
}
// Bytes returns the contents of the Buffer.
func (p *Buffer) Bytes() []byte { return p.buf }
/*
* Helper routines for simplifying the creation of optional fields of basic type.
*/
// Bool is a helper routine that allocates a new bool value
// to store v and returns a pointer to it.
func Bool(v bool) *bool {
return &v
}
// Int32 is a helper routine that allocates a new int32 value
// to store v and returns a pointer to it.
func Int32(v int32) *int32 {
return &v
}
// Int is a helper routine that allocates a new int32 value
// to store v and returns a pointer to it, but unlike Int32
// its argument value is an int.
func Int(v int) *int32 {
p := new(int32)
*p = int32(v)
return p
}
// Int64 is a helper routine that allocates a new int64 value
// to store v and returns a pointer to it.
func Int64(v int64) *int64 {
return &v
}
// Float32 is a helper routine that allocates a new float32 value
// to store v and returns a pointer to it.
func Float32(v float32) *float32 {
return &v
}
// Float64 is a helper routine that allocates a new float64 value
// to store v and returns a pointer to it.
func Float64(v float64) *float64 {
return &v
}
// Uint32 is a helper routine that allocates a new uint32 value
// to store v and returns a pointer to it.
func Uint32(v uint32) *uint32 {
return &v
}
// Uint64 is a helper routine that allocates a new uint64 value
// to store v and returns a pointer to it.
func Uint64(v uint64) *uint64 {
return &v
}
// String is a helper routine that allocates a new string value
// to store v and returns a pointer to it.
func String(v string) *string {
return &v
}
// EnumName is a helper function to simplify printing protocol buffer enums
// by name. Given an enum map and a value, it returns a useful string.
func EnumName(m map[int32]string, v int32) string {
s, ok := m[v]
if ok {
return s
}
return strconv.Itoa(int(v))
}
// UnmarshalJSONEnum is a helper function to simplify recovering enum int values
// from their JSON-encoded representation. Given a map from the enum's symbolic
// names to its int values, and a byte buffer containing the JSON-encoded
// value, it returns an int32 that can be cast to the enum type by the caller.
//
// The function can deal with both JSON representations, numeric and symbolic.
func UnmarshalJSONEnum(m map[string]int32, data []byte, enumName string) (int32, error) {
if data[0] == '"' {
// New style: enums are strings.
var repr string
if err := json.Unmarshal(data, &repr); err != nil {
return -1, err
}
val, ok := m[repr]
if !ok {
return 0, fmt.Errorf("unrecognized enum %s value %q", enumName, repr)
}
return val, nil
}
// Old style: enums are ints.
var val int32
if err := json.Unmarshal(data, &val); err != nil {
return 0, fmt.Errorf("cannot unmarshal %#q into enum %s", data, enumName)
}
return val, nil
}
// DebugPrint dumps the encoded data in b in a debugging format with a header
// including the string s. Used in testing but made available for general debugging.
func (p *Buffer) DebugPrint(s string, b []byte) {
var u uint64
obuf := p.buf
index := p.index
p.buf = b
p.index = 0
depth := 0
fmt.Printf("\n--- %s ---\n", s)
out:
for {
for i := 0; i < depth; i++ {
fmt.Print(" ")
}
index := p.index
if index == len(p.buf) {
break
}
op, err := p.DecodeVarint()
if err != nil {
fmt.Printf("%3d: fetching op err %v\n", index, err)
break out
}
tag := op >> 3
wire := op & 7
switch wire {
default:
fmt.Printf("%3d: t=%3d unknown wire=%d\n",
index, tag, wire)
break out
case WireBytes:
var r []byte
r, err = p.DecodeRawBytes(false)
if err != nil {
break out
}
fmt.Printf("%3d: t=%3d bytes [%d]", index, tag, len(r))
if len(r) <= 6 {
for i := 0; i < len(r); i++ {
fmt.Printf(" %.2x", r[i])
}
} else {
for i := 0; i < 3; i++ {
fmt.Printf(" %.2x", r[i])
}
fmt.Printf(" ..")
for i := len(r) - 3; i < len(r); i++ {
fmt.Printf(" %.2x", r[i])
}
}
fmt.Printf("\n")
case WireFixed32:
u, err = p.DecodeFixed32()
if err != nil {
fmt.Printf("%3d: t=%3d fix32 err %v\n", index, tag, err)
break out
}
fmt.Printf("%3d: t=%3d fix32 %d\n", index, tag, u)
case WireFixed64:
u, err = p.DecodeFixed64()
if err != nil {
fmt.Printf("%3d: t=%3d fix64 err %v\n", index, tag, err)
break out
}
fmt.Printf("%3d: t=%3d fix64 %d\n", index, tag, u)
case WireVarint:
u, err = p.DecodeVarint()
if err != nil {
fmt.Printf("%3d: t=%3d varint err %v\n", index, tag, err)
break out
}
fmt.Printf("%3d: t=%3d varint %d\n", index, tag, u)
case WireStartGroup:
fmt.Printf("%3d: t=%3d start\n", index, tag)
depth++
case WireEndGroup:
depth--
fmt.Printf("%3d: t=%3d end\n", index, tag)
}
}
if depth != 0 {
fmt.Printf("%3d: start-end not balanced %d\n", p.index, depth)
}
fmt.Printf("\n")
p.buf = obuf
p.index = index
}
// SetDefaults sets unset protocol buffer fields to their default values.
// It only modifies fields that are both unset and have defined defaults.
// It recursively sets default values in any non-nil sub-messages.
func SetDefaults(pb Message) {
setDefaults(reflect.ValueOf(pb), true, false)
}
// v is a pointer to a struct.
func setDefaults(v reflect.Value, recur, zeros bool) {
v = v.Elem()
defaultMu.RLock()
dm, ok := defaults[v.Type()]
defaultMu.RUnlock()
if !ok {
dm = buildDefaultMessage(v.Type())
defaultMu.Lock()
defaults[v.Type()] = dm
defaultMu.Unlock()
}
for _, sf := range dm.scalars {
f := v.Field(sf.index)
if !f.IsNil() {
// field already set
continue
}
dv := sf.value
if dv == nil && !zeros {
// no explicit default, and don't want to set zeros
continue
}
fptr := f.Addr().Interface() // **T
// TODO: Consider batching the allocations we do here.
switch sf.kind {
case reflect.Bool:
b := new(bool)
if dv != nil {
*b = dv.(bool)
}
*(fptr.(**bool)) = b
case reflect.Float32:
f := new(float32)
if dv != nil {
*f = dv.(float32)
}
*(fptr.(**float32)) = f
case reflect.Float64:
f := new(float64)
if dv != nil {
*f = dv.(float64)
}
*(fptr.(**float64)) = f
case reflect.Int32:
// might be an enum
if ft := f.Type(); ft != int32PtrType {
// enum
f.Set(reflect.New(ft.Elem()))
if dv != nil {
f.Elem().SetInt(int64(dv.(int32)))
}
} else {
// int32 field
i := new(int32)
if dv != nil {
*i = dv.(int32)
}
*(fptr.(**int32)) = i
}
case reflect.Int64:
i := new(int64)
if dv != nil {
*i = dv.(int64)
}
*(fptr.(**int64)) = i
case reflect.String:
s := new(string)
if dv != nil {
*s = dv.(string)
}
*(fptr.(**string)) = s
case reflect.Uint8:
// exceptional case: []byte
var b []byte
if dv != nil {
db := dv.([]byte)
b = make([]byte, len(db))
copy(b, db)
} else {
b = []byte{}
}
*(fptr.(*[]byte)) = b
case reflect.Uint32:
u := new(uint32)
if dv != nil {
*u = dv.(uint32)
}
*(fptr.(**uint32)) = u
case reflect.Uint64:
u := new(uint64)
if dv != nil {
*u = dv.(uint64)
}
*(fptr.(**uint64)) = u
default:
log.Printf("proto: can't set default for field %v (sf.kind=%v)", f, sf.kind)
}
}
for _, ni := range dm.nested {
f := v.Field(ni)
// f is *T or []*T or map[T]*T
switch f.Kind() {
case reflect.Ptr:
if f.IsNil() {
continue
}
setDefaults(f, recur, zeros)
case reflect.Slice:
for i := 0; i < f.Len(); i++ {
e := f.Index(i)
if e.IsNil() {
continue
}
setDefaults(e, recur, zeros)
}
case reflect.Map:
for _, k := range f.MapKeys() {
e := f.MapIndex(k)
if e.IsNil() {
continue
}
setDefaults(e, recur, zeros)
}
}
}
}
var (
// defaults maps a protocol buffer struct type to a slice of the fields,
// with its scalar fields set to their proto-declared non-zero default values.
defaultMu sync.RWMutex
defaults = make(map[reflect.Type]defaultMessage)
int32PtrType = reflect.TypeOf((*int32)(nil))
)
// defaultMessage represents information about the default values of a message.
type defaultMessage struct {
scalars []scalarField
nested []int // struct field index of nested messages
}
type scalarField struct {
index int // struct field index
kind reflect.Kind // element type (the T in *T or []T)
value interface{} // the proto-declared default value, or nil
}
// t is a struct type.
func buildDefaultMessage(t reflect.Type) (dm defaultMessage) {
sprop := GetProperties(t)
for _, prop := range sprop.Prop {
fi, ok := sprop.decoderTags.get(prop.Tag)
if !ok {
// XXX_unrecognized
continue
}
ft := t.Field(fi).Type
sf, nested, err := fieldDefault(ft, prop)
switch {
case err != nil:
log.Print(err)
case nested:
dm.nested = append(dm.nested, fi)
case sf != nil:
sf.index = fi
dm.scalars = append(dm.scalars, *sf)
}
}
return dm
}
// fieldDefault returns the scalarField for field type ft.
// sf will be nil if the field can not have a default.
// nestedMessage will be true if this is a nested message.
// Note that sf.index is not set on return.
func fieldDefault(ft reflect.Type, prop *Properties) (sf *scalarField, nestedMessage bool, err error) {
var canHaveDefault bool
switch ft.Kind() {
case reflect.Ptr:
if ft.Elem().Kind() == reflect.Struct {
nestedMessage = true
} else {
canHaveDefault = true // proto2 scalar field
}
case reflect.Slice:
switch ft.Elem().Kind() {
case reflect.Ptr:
nestedMessage = true // repeated message
case reflect.Uint8:
canHaveDefault = true // bytes field
}
case reflect.Map:
if ft.Elem().Kind() == reflect.Ptr {
nestedMessage = true // map with message values
}
}
if !canHaveDefault {
if nestedMessage {
return nil, true, nil
}
return nil, false, nil
}
// We now know that ft is a pointer or slice.
sf = &scalarField{kind: ft.Elem().Kind()}
// scalar fields without defaults
if !prop.HasDefault {
return sf, false, nil
}
// a scalar field: either *T or []byte
switch ft.Elem().Kind() {
case reflect.Bool:
x, err := strconv.ParseBool(prop.Default)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default bool %q: %v", prop.Default, err)
}
sf.value = x
case reflect.Float32:
x, err := strconv.ParseFloat(prop.Default, 32)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default float32 %q: %v", prop.Default, err)
}
sf.value = float32(x)
case reflect.Float64:
x, err := strconv.ParseFloat(prop.Default, 64)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default float64 %q: %v", prop.Default, err)
}
sf.value = x
case reflect.Int32:
x, err := strconv.ParseInt(prop.Default, 10, 32)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default int32 %q: %v", prop.Default, err)
}
sf.value = int32(x)
case reflect.Int64:
x, err := strconv.ParseInt(prop.Default, 10, 64)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default int64 %q: %v", prop.Default, err)
}
sf.value = x
case reflect.String:
sf.value = prop.Default
case reflect.Uint8:
// []byte (not *uint8)
sf.value = []byte(prop.Default)
case reflect.Uint32:
x, err := strconv.ParseUint(prop.Default, 10, 32)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default uint32 %q: %v", prop.Default, err)
}
sf.value = uint32(x)
case reflect.Uint64:
x, err := strconv.ParseUint(prop.Default, 10, 64)
if err != nil {
return nil, false, fmt.Errorf("proto: bad default uint64 %q: %v", prop.Default, err)
}
sf.value = x
default:
return nil, false, fmt.Errorf("proto: unhandled def kind %v", ft.Elem().Kind())
}
return sf, false, nil
}
// Map fields may have key types of non-float scalars, strings and enums.
// The easiest way to sort them in some deterministic order is to use fmt.
// If this turns out to be inefficient we can always consider other options,
// such as doing a Schwartzian transform.
func mapKeys(vs []reflect.Value) sort.Interface {
s := mapKeySorter{
vs: vs,
// default Less function: textual comparison
less: func(a, b reflect.Value) bool {
return fmt.Sprint(a.Interface()) < fmt.Sprint(b.Interface())
},
}
// Type specialization per https://developers.google.com/protocol-buffers/docs/proto#maps;
// numeric keys are sorted numerically.
if len(vs) == 0 {
return s
}
switch vs[0].Kind() {
case reflect.Int32, reflect.Int64:
s.less = func(a, b reflect.Value) bool { return a.Int() < b.Int() }
case reflect.Uint32, reflect.Uint64:
s.less = func(a, b reflect.Value) bool { return a.Uint() < b.Uint() }
}
return s
}
type mapKeySorter struct {
vs []reflect.Value
less func(a, b reflect.Value) bool
}
func (s mapKeySorter) Len() int { return len(s.vs) }
func (s mapKeySorter) Swap(i, j int) { s.vs[i], s.vs[j] = s.vs[j], s.vs[i] }
func (s mapKeySorter) Less(i, j int) bool {
return s.less(s.vs[i], s.vs[j])
}
// isProto3Zero reports whether v is a zero proto3 value.
func isProto3Zero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Bool:
return !v.Bool()
case reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.String:
return v.String() == ""
}
return false
}
// ProtoPackageIsVersion2 is referenced from generated protocol buffer files
// to assert that that code is compatible with this version of the proto package.
const ProtoPackageIsVersion2 = true
// ProtoPackageIsVersion1 is referenced from generated protocol buffer files
// to assert that that code is compatible with this version of the proto package.
const ProtoPackageIsVersion1 = true
+311
View File
@@ -0,0 +1,311 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2010 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package proto
/*
* Support for message sets.
*/
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"sort"
)
// errNoMessageTypeID occurs when a protocol buffer does not have a message type ID.
// A message type ID is required for storing a protocol buffer in a message set.
var errNoMessageTypeID = errors.New("proto does not have a message type ID")
// The first two types (_MessageSet_Item and messageSet)
// model what the protocol compiler produces for the following protocol message:
// message MessageSet {
// repeated group Item = 1 {
// required int32 type_id = 2;
// required string message = 3;
// };
// }
// That is the MessageSet wire format. We can't use a proto to generate these
// because that would introduce a circular dependency between it and this package.
type _MessageSet_Item struct {
TypeId *int32 `protobuf:"varint,2,req,name=type_id"`
Message []byte `protobuf:"bytes,3,req,name=message"`
}
type messageSet struct {
Item []*_MessageSet_Item `protobuf:"group,1,rep"`
XXX_unrecognized []byte
// TODO: caching?
}
// Make sure messageSet is a Message.
var _ Message = (*messageSet)(nil)
// messageTypeIder is an interface satisfied by a protocol buffer type
// that may be stored in a MessageSet.
type messageTypeIder interface {
MessageTypeId() int32
}
func (ms *messageSet) find(pb Message) *_MessageSet_Item {
mti, ok := pb.(messageTypeIder)
if !ok {
return nil
}
id := mti.MessageTypeId()
for _, item := range ms.Item {
if *item.TypeId == id {
return item
}
}
return nil
}
func (ms *messageSet) Has(pb Message) bool {
if ms.find(pb) != nil {
return true
}
return false
}
func (ms *messageSet) Unmarshal(pb Message) error {
if item := ms.find(pb); item != nil {
return Unmarshal(item.Message, pb)
}
if _, ok := pb.(messageTypeIder); !ok {
return errNoMessageTypeID
}
return nil // TODO: return error instead?
}
func (ms *messageSet) Marshal(pb Message) error {
msg, err := Marshal(pb)
if err != nil {
return err
}
if item := ms.find(pb); item != nil {
// reuse existing item
item.Message = msg
return nil
}
mti, ok := pb.(messageTypeIder)
if !ok {
return errNoMessageTypeID
}
mtid := mti.MessageTypeId()
ms.Item = append(ms.Item, &_MessageSet_Item{
TypeId: &mtid,
Message: msg,
})
return nil
}
func (ms *messageSet) Reset() { *ms = messageSet{} }
func (ms *messageSet) String() string { return CompactTextString(ms) }
func (*messageSet) ProtoMessage() {}
// Support for the message_set_wire_format message option.
func skipVarint(buf []byte) []byte {
i := 0
for ; buf[i]&0x80 != 0; i++ {
}
return buf[i+1:]
}
// MarshalMessageSet encodes the extension map represented by m in the message set wire format.
// It is called by generated Marshal methods on protocol buffer messages with the message_set_wire_format option.
func MarshalMessageSet(exts interface{}) ([]byte, error) {
var m map[int32]Extension
switch exts := exts.(type) {
case *XXX_InternalExtensions:
if err := encodeExtensions(exts); err != nil {
return nil, err
}
m, _ = exts.extensionsRead()
case map[int32]Extension:
if err := encodeExtensionsMap(exts); err != nil {
return nil, err
}
m = exts
default:
return nil, errors.New("proto: not an extension map")
}
// Sort extension IDs to provide a deterministic encoding.
// See also enc_map in encode.go.
ids := make([]int, 0, len(m))
for id := range m {
ids = append(ids, int(id))
}
sort.Ints(ids)
ms := &messageSet{Item: make([]*_MessageSet_Item, 0, len(m))}
for _, id := range ids {
e := m[int32(id)]
// Remove the wire type and field number varint, as well as the length varint.
msg := skipVarint(skipVarint(e.enc))
ms.Item = append(ms.Item, &_MessageSet_Item{
TypeId: Int32(int32(id)),
Message: msg,
})
}
return Marshal(ms)
}
// UnmarshalMessageSet decodes the extension map encoded in buf in the message set wire format.
// It is called by generated Unmarshal methods on protocol buffer messages with the message_set_wire_format option.
func UnmarshalMessageSet(buf []byte, exts interface{}) error {
var m map[int32]Extension
switch exts := exts.(type) {
case *XXX_InternalExtensions:
m = exts.extensionsWrite()
case map[int32]Extension:
m = exts
default:
return errors.New("proto: not an extension map")
}
ms := new(messageSet)
if err := Unmarshal(buf, ms); err != nil {
return err
}
for _, item := range ms.Item {
id := *item.TypeId
msg := item.Message
// Restore wire type and field number varint, plus length varint.
// Be careful to preserve duplicate items.
b := EncodeVarint(uint64(id)<<3 | WireBytes)
if ext, ok := m[id]; ok {
// Existing data; rip off the tag and length varint
// so we join the new data correctly.
// We can assume that ext.enc is set because we are unmarshaling.
o := ext.enc[len(b):] // skip wire type and field number
_, n := DecodeVarint(o) // calculate length of length varint
o = o[n:] // skip length varint
msg = append(o, msg...) // join old data and new data
}
b = append(b, EncodeVarint(uint64(len(msg)))...)
b = append(b, msg...)
m[id] = Extension{enc: b}
}
return nil
}
// MarshalMessageSetJSON encodes the extension map represented by m in JSON format.
// It is called by generated MarshalJSON methods on protocol buffer messages with the message_set_wire_format option.
func MarshalMessageSetJSON(exts interface{}) ([]byte, error) {
var m map[int32]Extension
switch exts := exts.(type) {
case *XXX_InternalExtensions:
m, _ = exts.extensionsRead()
case map[int32]Extension:
m = exts
default:
return nil, errors.New("proto: not an extension map")
}
var b bytes.Buffer
b.WriteByte('{')
// Process the map in key order for deterministic output.
ids := make([]int32, 0, len(m))
for id := range m {
ids = append(ids, id)
}
sort.Sort(int32Slice(ids)) // int32Slice defined in text.go
for i, id := range ids {
ext := m[id]
if i > 0 {
b.WriteByte(',')
}
msd, ok := messageSetMap[id]
if !ok {
// Unknown type; we can't render it, so skip it.
continue
}
fmt.Fprintf(&b, `"[%s]":`, msd.name)
x := ext.value
if x == nil {
x = reflect.New(msd.t.Elem()).Interface()
if err := Unmarshal(ext.enc, x.(Message)); err != nil {
return nil, err
}
}
d, err := json.Marshal(x)
if err != nil {
return nil, err
}
b.Write(d)
}
b.WriteByte('}')
return b.Bytes(), nil
}
// UnmarshalMessageSetJSON decodes the extension map encoded in buf in JSON format.
// It is called by generated UnmarshalJSON methods on protocol buffer messages with the message_set_wire_format option.
func UnmarshalMessageSetJSON(buf []byte, exts interface{}) error {
// Common-case fast path.
if len(buf) == 0 || bytes.Equal(buf, []byte("{}")) {
return nil
}
// This is fairly tricky, and it's not clear that it is needed.
return errors.New("TODO: UnmarshalMessageSetJSON not yet implemented")
}
// A global registry of types that can be used in a MessageSet.
var messageSetMap = make(map[int32]messageSetDesc)
type messageSetDesc struct {
t reflect.Type // pointer to struct
name string
}
// RegisterMessageSetType is called from the generated code.
func RegisterMessageSetType(m Message, fieldNum int32, name string) {
messageSetMap[fieldNum] = messageSetDesc{
t: reflect.TypeOf(m),
name: name,
}
}
+484
View File
@@ -0,0 +1,484 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2012 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build appengine js
// This file contains an implementation of proto field accesses using package reflect.
// It is slower than the code in pointer_unsafe.go but it avoids package unsafe and can
// be used on App Engine.
package proto
import (
"math"
"reflect"
)
// A structPointer is a pointer to a struct.
type structPointer struct {
v reflect.Value
}
// toStructPointer returns a structPointer equivalent to the given reflect value.
// The reflect value must itself be a pointer to a struct.
func toStructPointer(v reflect.Value) structPointer {
return structPointer{v}
}
// IsNil reports whether p is nil.
func structPointer_IsNil(p structPointer) bool {
return p.v.IsNil()
}
// Interface returns the struct pointer as an interface value.
func structPointer_Interface(p structPointer, _ reflect.Type) interface{} {
return p.v.Interface()
}
// A field identifies a field in a struct, accessible from a structPointer.
// In this implementation, a field is identified by the sequence of field indices
// passed to reflect's FieldByIndex.
type field []int
// toField returns a field equivalent to the given reflect field.
func toField(f *reflect.StructField) field {
return f.Index
}
// invalidField is an invalid field identifier.
var invalidField = field(nil)
// IsValid reports whether the field identifier is valid.
func (f field) IsValid() bool { return f != nil }
// field returns the given field in the struct as a reflect value.
func structPointer_field(p structPointer, f field) reflect.Value {
// Special case: an extension map entry with a value of type T
// passes a *T to the struct-handling code with a zero field,
// expecting that it will be treated as equivalent to *struct{ X T },
// which has the same memory layout. We have to handle that case
// specially, because reflect will panic if we call FieldByIndex on a
// non-struct.
if f == nil {
return p.v.Elem()
}
return p.v.Elem().FieldByIndex(f)
}
// ifield returns the given field in the struct as an interface value.
func structPointer_ifield(p structPointer, f field) interface{} {
return structPointer_field(p, f).Addr().Interface()
}
// Bytes returns the address of a []byte field in the struct.
func structPointer_Bytes(p structPointer, f field) *[]byte {
return structPointer_ifield(p, f).(*[]byte)
}
// BytesSlice returns the address of a [][]byte field in the struct.
func structPointer_BytesSlice(p structPointer, f field) *[][]byte {
return structPointer_ifield(p, f).(*[][]byte)
}
// Bool returns the address of a *bool field in the struct.
func structPointer_Bool(p structPointer, f field) **bool {
return structPointer_ifield(p, f).(**bool)
}
// BoolVal returns the address of a bool field in the struct.
func structPointer_BoolVal(p structPointer, f field) *bool {
return structPointer_ifield(p, f).(*bool)
}
// BoolSlice returns the address of a []bool field in the struct.
func structPointer_BoolSlice(p structPointer, f field) *[]bool {
return structPointer_ifield(p, f).(*[]bool)
}
// String returns the address of a *string field in the struct.
func structPointer_String(p structPointer, f field) **string {
return structPointer_ifield(p, f).(**string)
}
// StringVal returns the address of a string field in the struct.
func structPointer_StringVal(p structPointer, f field) *string {
return structPointer_ifield(p, f).(*string)
}
// StringSlice returns the address of a []string field in the struct.
func structPointer_StringSlice(p structPointer, f field) *[]string {
return structPointer_ifield(p, f).(*[]string)
}
// Extensions returns the address of an extension map field in the struct.
func structPointer_Extensions(p structPointer, f field) *XXX_InternalExtensions {
return structPointer_ifield(p, f).(*XXX_InternalExtensions)
}
// ExtMap returns the address of an extension map field in the struct.
func structPointer_ExtMap(p structPointer, f field) *map[int32]Extension {
return structPointer_ifield(p, f).(*map[int32]Extension)
}
// NewAt returns the reflect.Value for a pointer to a field in the struct.
func structPointer_NewAt(p structPointer, f field, typ reflect.Type) reflect.Value {
return structPointer_field(p, f).Addr()
}
// SetStructPointer writes a *struct field in the struct.
func structPointer_SetStructPointer(p structPointer, f field, q structPointer) {
structPointer_field(p, f).Set(q.v)
}
// GetStructPointer reads a *struct field in the struct.
func structPointer_GetStructPointer(p structPointer, f field) structPointer {
return structPointer{structPointer_field(p, f)}
}
// StructPointerSlice the address of a []*struct field in the struct.
func structPointer_StructPointerSlice(p structPointer, f field) structPointerSlice {
return structPointerSlice{structPointer_field(p, f)}
}
// A structPointerSlice represents the address of a slice of pointers to structs
// (themselves messages or groups). That is, v.Type() is *[]*struct{...}.
type structPointerSlice struct {
v reflect.Value
}
func (p structPointerSlice) Len() int { return p.v.Len() }
func (p structPointerSlice) Index(i int) structPointer { return structPointer{p.v.Index(i)} }
func (p structPointerSlice) Append(q structPointer) {
p.v.Set(reflect.Append(p.v, q.v))
}
var (
int32Type = reflect.TypeOf(int32(0))
uint32Type = reflect.TypeOf(uint32(0))
float32Type = reflect.TypeOf(float32(0))
int64Type = reflect.TypeOf(int64(0))
uint64Type = reflect.TypeOf(uint64(0))
float64Type = reflect.TypeOf(float64(0))
)
// A word32 represents a field of type *int32, *uint32, *float32, or *enum.
// That is, v.Type() is *int32, *uint32, *float32, or *enum and v is assignable.
type word32 struct {
v reflect.Value
}
// IsNil reports whether p is nil.
func word32_IsNil(p word32) bool {
return p.v.IsNil()
}
// Set sets p to point at a newly allocated word with bits set to x.
func word32_Set(p word32, o *Buffer, x uint32) {
t := p.v.Type().Elem()
switch t {
case int32Type:
if len(o.int32s) == 0 {
o.int32s = make([]int32, uint32PoolSize)
}
o.int32s[0] = int32(x)
p.v.Set(reflect.ValueOf(&o.int32s[0]))
o.int32s = o.int32s[1:]
return
case uint32Type:
if len(o.uint32s) == 0 {
o.uint32s = make([]uint32, uint32PoolSize)
}
o.uint32s[0] = x
p.v.Set(reflect.ValueOf(&o.uint32s[0]))
o.uint32s = o.uint32s[1:]
return
case float32Type:
if len(o.float32s) == 0 {
o.float32s = make([]float32, uint32PoolSize)
}
o.float32s[0] = math.Float32frombits(x)
p.v.Set(reflect.ValueOf(&o.float32s[0]))
o.float32s = o.float32s[1:]
return
}
// must be enum
p.v.Set(reflect.New(t))
p.v.Elem().SetInt(int64(int32(x)))
}
// Get gets the bits pointed at by p, as a uint32.
func word32_Get(p word32) uint32 {
elem := p.v.Elem()
switch elem.Kind() {
case reflect.Int32:
return uint32(elem.Int())
case reflect.Uint32:
return uint32(elem.Uint())
case reflect.Float32:
return math.Float32bits(float32(elem.Float()))
}
panic("unreachable")
}
// Word32 returns a reference to a *int32, *uint32, *float32, or *enum field in the struct.
func structPointer_Word32(p structPointer, f field) word32 {
return word32{structPointer_field(p, f)}
}
// A word32Val represents a field of type int32, uint32, float32, or enum.
// That is, v.Type() is int32, uint32, float32, or enum and v is assignable.
type word32Val struct {
v reflect.Value
}
// Set sets *p to x.
func word32Val_Set(p word32Val, x uint32) {
switch p.v.Type() {
case int32Type:
p.v.SetInt(int64(x))
return
case uint32Type:
p.v.SetUint(uint64(x))
return
case float32Type:
p.v.SetFloat(float64(math.Float32frombits(x)))
return
}
// must be enum
p.v.SetInt(int64(int32(x)))
}
// Get gets the bits pointed at by p, as a uint32.
func word32Val_Get(p word32Val) uint32 {
elem := p.v
switch elem.Kind() {
case reflect.Int32:
return uint32(elem.Int())
case reflect.Uint32:
return uint32(elem.Uint())
case reflect.Float32:
return math.Float32bits(float32(elem.Float()))
}
panic("unreachable")
}
// Word32Val returns a reference to a int32, uint32, float32, or enum field in the struct.
func structPointer_Word32Val(p structPointer, f field) word32Val {
return word32Val{structPointer_field(p, f)}
}
// A word32Slice is a slice of 32-bit values.
// That is, v.Type() is []int32, []uint32, []float32, or []enum.
type word32Slice struct {
v reflect.Value
}
func (p word32Slice) Append(x uint32) {
n, m := p.v.Len(), p.v.Cap()
if n < m {
p.v.SetLen(n + 1)
} else {
t := p.v.Type().Elem()
p.v.Set(reflect.Append(p.v, reflect.Zero(t)))
}
elem := p.v.Index(n)
switch elem.Kind() {
case reflect.Int32:
elem.SetInt(int64(int32(x)))
case reflect.Uint32:
elem.SetUint(uint64(x))
case reflect.Float32:
elem.SetFloat(float64(math.Float32frombits(x)))
}
}
func (p word32Slice) Len() int {
return p.v.Len()
}
func (p word32Slice) Index(i int) uint32 {
elem := p.v.Index(i)
switch elem.Kind() {
case reflect.Int32:
return uint32(elem.Int())
case reflect.Uint32:
return uint32(elem.Uint())
case reflect.Float32:
return math.Float32bits(float32(elem.Float()))
}
panic("unreachable")
}
// Word32Slice returns a reference to a []int32, []uint32, []float32, or []enum field in the struct.
func structPointer_Word32Slice(p structPointer, f field) word32Slice {
return word32Slice{structPointer_field(p, f)}
}
// word64 is like word32 but for 64-bit values.
type word64 struct {
v reflect.Value
}
func word64_Set(p word64, o *Buffer, x uint64) {
t := p.v.Type().Elem()
switch t {
case int64Type:
if len(o.int64s) == 0 {
o.int64s = make([]int64, uint64PoolSize)
}
o.int64s[0] = int64(x)
p.v.Set(reflect.ValueOf(&o.int64s[0]))
o.int64s = o.int64s[1:]
return
case uint64Type:
if len(o.uint64s) == 0 {
o.uint64s = make([]uint64, uint64PoolSize)
}
o.uint64s[0] = x
p.v.Set(reflect.ValueOf(&o.uint64s[0]))
o.uint64s = o.uint64s[1:]
return
case float64Type:
if len(o.float64s) == 0 {
o.float64s = make([]float64, uint64PoolSize)
}
o.float64s[0] = math.Float64frombits(x)
p.v.Set(reflect.ValueOf(&o.float64s[0]))
o.float64s = o.float64s[1:]
return
}
panic("unreachable")
}
func word64_IsNil(p word64) bool {
return p.v.IsNil()
}
func word64_Get(p word64) uint64 {
elem := p.v.Elem()
switch elem.Kind() {
case reflect.Int64:
return uint64(elem.Int())
case reflect.Uint64:
return elem.Uint()
case reflect.Float64:
return math.Float64bits(elem.Float())
}
panic("unreachable")
}
func structPointer_Word64(p structPointer, f field) word64 {
return word64{structPointer_field(p, f)}
}
// word64Val is like word32Val but for 64-bit values.
type word64Val struct {
v reflect.Value
}
func word64Val_Set(p word64Val, o *Buffer, x uint64) {
switch p.v.Type() {
case int64Type:
p.v.SetInt(int64(x))
return
case uint64Type:
p.v.SetUint(x)
return
case float64Type:
p.v.SetFloat(math.Float64frombits(x))
return
}
panic("unreachable")
}
func word64Val_Get(p word64Val) uint64 {
elem := p.v
switch elem.Kind() {
case reflect.Int64:
return uint64(elem.Int())
case reflect.Uint64:
return elem.Uint()
case reflect.Float64:
return math.Float64bits(elem.Float())
}
panic("unreachable")
}
func structPointer_Word64Val(p structPointer, f field) word64Val {
return word64Val{structPointer_field(p, f)}
}
type word64Slice struct {
v reflect.Value
}
func (p word64Slice) Append(x uint64) {
n, m := p.v.Len(), p.v.Cap()
if n < m {
p.v.SetLen(n + 1)
} else {
t := p.v.Type().Elem()
p.v.Set(reflect.Append(p.v, reflect.Zero(t)))
}
elem := p.v.Index(n)
switch elem.Kind() {
case reflect.Int64:
elem.SetInt(int64(int64(x)))
case reflect.Uint64:
elem.SetUint(uint64(x))
case reflect.Float64:
elem.SetFloat(float64(math.Float64frombits(x)))
}
}
func (p word64Slice) Len() int {
return p.v.Len()
}
func (p word64Slice) Index(i int) uint64 {
elem := p.v.Index(i)
switch elem.Kind() {
case reflect.Int64:
return uint64(elem.Int())
case reflect.Uint64:
return uint64(elem.Uint())
case reflect.Float64:
return math.Float64bits(float64(elem.Float()))
}
panic("unreachable")
}
func structPointer_Word64Slice(p structPointer, f field) word64Slice {
return word64Slice{structPointer_field(p, f)}
}
+270
View File
@@ -0,0 +1,270 @@
// Go support for Protocol Buffers - Google's data interchange format
//
// Copyright 2012 The Go Authors. All rights reserved.
// https://github.com/golang/protobuf
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build !appengine,!js
// This file contains the implementation of the proto field accesses using package unsafe.
package proto
import (
"reflect"
"unsafe"
)
// NOTE: These type_Foo functions would more idiomatically be methods,
// but Go does not allow methods on pointer types, and we must preserve
// some pointer type for the garbage collector. We use these
// funcs with clunky names as our poor approximation to methods.
//
// An alternative would be
// type structPointer struct { p unsafe.Pointer }
// but that does not registerize as well.
// A structPointer is a pointer to a struct.
type structPointer unsafe.Pointer
// toStructPointer returns a structPointer equivalent to the given reflect value.
func toStructPointer(v reflect.Value) structPointer {
return structPointer(unsafe.Pointer(v.Pointer()))
}
// IsNil reports whether p is nil.
func structPointer_IsNil(p structPointer) bool {
return p == nil
}
// Interface returns the struct pointer, assumed to have element type t,
// as an interface value.
func structPointer_Interface(p structPointer, t reflect.Type) interface{} {
return reflect.NewAt(t, unsafe.Pointer(p)).Interface()
}
// A field identifies a field in a struct, accessible from a structPointer.
// In this implementation, a field is identified by its byte offset from the start of the struct.
type field uintptr
// toField returns a field equivalent to the given reflect field.
func toField(f *reflect.StructField) field {
return field(f.Offset)
}
// invalidField is an invalid field identifier.
const invalidField = ^field(0)
// IsValid reports whether the field identifier is valid.
func (f field) IsValid() bool {
return f != ^field(0)
}
// Bytes returns the address of a []byte field in the struct.
func structPointer_Bytes(p structPointer, f field) *[]byte {
return (*[]byte)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// BytesSlice returns the address of a [][]byte field in the struct.
func structPointer_BytesSlice(p structPointer, f field) *[][]byte {
return (*[][]byte)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// Bool returns the address of a *bool field in the struct.
func structPointer_Bool(p structPointer, f field) **bool {
return (**bool)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// BoolVal returns the address of a bool field in the struct.
func structPointer_BoolVal(p structPointer, f field) *bool {
return (*bool)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// BoolSlice returns the address of a []bool field in the struct.
func structPointer_BoolSlice(p structPointer, f field) *[]bool {
return (*[]bool)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// String returns the address of a *string field in the struct.
func structPointer_String(p structPointer, f field) **string {
return (**string)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// StringVal returns the address of a string field in the struct.
func structPointer_StringVal(p structPointer, f field) *string {
return (*string)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// StringSlice returns the address of a []string field in the struct.
func structPointer_StringSlice(p structPointer, f field) *[]string {
return (*[]string)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// ExtMap returns the address of an extension map field in the struct.
func structPointer_Extensions(p structPointer, f field) *XXX_InternalExtensions {
return (*XXX_InternalExtensions)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
func structPointer_ExtMap(p structPointer, f field) *map[int32]Extension {
return (*map[int32]Extension)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// NewAt returns the reflect.Value for a pointer to a field in the struct.
func structPointer_NewAt(p structPointer, f field, typ reflect.Type) reflect.Value {
return reflect.NewAt(typ, unsafe.Pointer(uintptr(p)+uintptr(f)))
}
// SetStructPointer writes a *struct field in the struct.
func structPointer_SetStructPointer(p structPointer, f field, q structPointer) {
*(*structPointer)(unsafe.Pointer(uintptr(p) + uintptr(f))) = q
}
// GetStructPointer reads a *struct field in the struct.
func structPointer_GetStructPointer(p structPointer, f field) structPointer {
return *(*structPointer)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// StructPointerSlice the address of a []*struct field in the struct.
func structPointer_StructPointerSlice(p structPointer, f field) *structPointerSlice {
return (*structPointerSlice)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// A structPointerSlice represents a slice of pointers to structs (themselves submessages or groups).
type structPointerSlice []structPointer
func (v *structPointerSlice) Len() int { return len(*v) }
func (v *structPointerSlice) Index(i int) structPointer { return (*v)[i] }
func (v *structPointerSlice) Append(p structPointer) { *v = append(*v, p) }
// A word32 is the address of a "pointer to 32-bit value" field.
type word32 **uint32
// IsNil reports whether *v is nil.
func word32_IsNil(p word32) bool {
return *p == nil
}
// Set sets *v to point at a newly allocated word set to x.
func word32_Set(p word32, o *Buffer, x uint32) {
if len(o.uint32s) == 0 {
o.uint32s = make([]uint32, uint32PoolSize)
}
o.uint32s[0] = x
*p = &o.uint32s[0]
o.uint32s = o.uint32s[1:]
}
// Get gets the value pointed at by *v.
func word32_Get(p word32) uint32 {
return **p
}
// Word32 returns the address of a *int32, *uint32, *float32, or *enum field in the struct.
func structPointer_Word32(p structPointer, f field) word32 {
return word32((**uint32)(unsafe.Pointer(uintptr(p) + uintptr(f))))
}
// A word32Val is the address of a 32-bit value field.
type word32Val *uint32
// Set sets *p to x.
func word32Val_Set(p word32Val, x uint32) {
*p = x
}
// Get gets the value pointed at by p.
func word32Val_Get(p word32Val) uint32 {
return *p
}
// Word32Val returns the address of a *int32, *uint32, *float32, or *enum field in the struct.
func structPointer_Word32Val(p structPointer, f field) word32Val {
return word32Val((*uint32)(unsafe.Pointer(uintptr(p) + uintptr(f))))
}
// A word32Slice is a slice of 32-bit values.
type word32Slice []uint32
func (v *word32Slice) Append(x uint32) { *v = append(*v, x) }
func (v *word32Slice) Len() int { return len(*v) }
func (v *word32Slice) Index(i int) uint32 { return (*v)[i] }
// Word32Slice returns the address of a []int32, []uint32, []float32, or []enum field in the struct.
func structPointer_Word32Slice(p structPointer, f field) *word32Slice {
return (*word32Slice)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}
// word64 is like word32 but for 64-bit values.
type word64 **uint64
func word64_Set(p word64, o *Buffer, x uint64) {
if len(o.uint64s) == 0 {
o.uint64s = make([]uint64, uint64PoolSize)
}
o.uint64s[0] = x
*p = &o.uint64s[0]
o.uint64s = o.uint64s[1:]
}
func word64_IsNil(p word64) bool {
return *p == nil
}
func word64_Get(p word64) uint64 {
return **p
}
func structPointer_Word64(p structPointer, f field) word64 {
return word64((**uint64)(unsafe.Pointer(uintptr(p) + uintptr(f))))
}
// word64Val is like word32Val but for 64-bit values.
type word64Val *uint64
func word64Val_Set(p word64Val, o *Buffer, x uint64) {
*p = x
}
func word64Val_Get(p word64Val) uint64 {
return *p
}
func structPointer_Word64Val(p structPointer, f field) word64Val {
return word64Val((*uint64)(unsafe.Pointer(uintptr(p) + uintptr(f))))
}
// word64Slice is like word32Slice but for 64-bit values.
type word64Slice []uint64
func (v *word64Slice) Append(x uint64) { *v = append(*v, x) }
func (v *word64Slice) Len() int { return len(*v) }
func (v *word64Slice) Index(i int) uint64 { return (*v)[i] }
func structPointer_Word64Slice(p structPointer, f field) *word64Slice {
return (*word64Slice)(unsafe.Pointer(uintptr(p) + uintptr(f)))
}

Some files were not shown because too many files have changed in this diff Show More