Compare commits

...
20 Commits
Author SHA1 Message Date
ouqiang 32caa5a4b7 增加v1.4 ChangeLog 2018-02-04 11:35:12 +08:00
ouqiang 738b168689 使用makefile构建应用 2018-02-04 11:28:48 +08:00
ouqiang e1fa3cf645 新增配置项: 同时运行的任务数量 2018-01-30 22:36:18 +08:00
ouqiang 3ed64f9f22 性能优化: 当任务较多时, gocron启动慢 2018-01-30 21:16:44 +08:00
ouqiang 94c9963e81 使用goimports格式化代码 2018-01-30 19:26:04 +08:00
ouqiang 4c940c2088 模板分隔符改为"{% %}" 2018-01-28 15:48:37 +08:00
ouqiang 50638e6dfa HTTP任务支持POST请求 Close #19 2018-01-27 18:08:46 +08:00
ouqiang f081d89fb4 支持后台手动停止运行中的shell任务 2018-01-27 11:10:08 +08:00
ouqiang fd39434446 CI构建最低版本要求1.9 2018-01-26 23:17:48 +08:00
ouqiang 72349248ae 增加dockerfile 2018-01-26 23:13:58 +08:00
ouqiang 02e525ab83 等待任务结束从轮询改为WaitGroup 2018-01-26 22:46:36 +08:00
ouqiang 0950fc69f9 data, conf, log目录不存在自动创建 2018-01-26 21:08:52 +08:00
ouqiang 750fb49a37 bugfix: API访问报403错误 2017-12-25 23:55:23 +08:00
ouqiang 68d569a6de feat($task): 任务失败重试间隔时间可由用户自定义, 如果未配置则按默认重试策略执行
Close #30
2017-12-09 11:01:48 +08:00
ouqiang 3fd5b2ee55 feat($task): 删除任务弹出确认提示
Close #28
2017-12-09 10:16:30 +08:00
qiang.ouandGitHub e9b475a230 bugfix: API调用报403错误 2017-11-17 13:42:02 +08:00
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
91 changed files with 4674 additions and 4134 deletions
+1
View File
@@ -0,0 +1 @@
.git
+8 -5
View File
@@ -23,16 +23,19 @@ _testmain.go
*.test
*.prof
.DS_Store
.idea
log/*
data/*
conf/*
log
data
conf
profile/*
public/resource/javascript/vue.js
gocron
/gocron
gocron.exe
gocron-node
/gocron-node
gocron-node.exe
/bin
/packages
node_modules
package.json
+1 -1
View File
@@ -1,6 +1,6 @@
language: go
go:
- 1.7.x
- 1.9.x
script: go test `go list ./... | grep -v vendor`
notifications:
+9
View File
@@ -0,0 +1,9 @@
FROM alpine:latest
COPY gocron /usr/local/gocron
WORKDIR /usr/local/gocron
EXPOSE 5920
ENTRYPOINT ["/usr/local/gocron/gocron", "web"]
+36 -22
View File
@@ -1,5 +1,8 @@
[![Build Status](https://travis-ci.org/ouqiang/gocron.png)](https://travis-ci.org/ouqiang/gocron)
# gocron - 定时任务管理系统
[![Build Status](https://travis-ci.org/ouqiang/gocron.png)](https://travis-ci.org/ouqiang/gocron)
[![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)
@@ -7,23 +10,24 @@
原有的延时任务拆分为独立项目[延迟队列](https://github.com/ouqiang/delay-queue)
## 功能特性
* Web界面管理定时任务, 支持动态添加、删除任务
* crontab时间表达式精确到秒
* 任务执行失败重试设置
* 任务超时设置
* 任务依赖配置
* 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)
![流程图](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot/scheduler.png)
![任务](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot/task.png)
![Slack](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot/notification.png)
### 支持平台
> Windows、Linux、Mac OS
@@ -33,7 +37,7 @@
## 下载
[v1.2.2](https://github.com/ouqiang/gocron/releases/tag/v1.2.2)
[releases](https://github.com/ouqiang/gocron/releases)
[版本升级](https://github.com/ouqiang/gocron/wiki/版本升级)
@@ -52,12 +56,12 @@
4. 浏览器访问 http://localhost:5920
### 源码安装
1. `go`语言版本1.7+
1. `go`语言版本1.9+
2. `go get -d github.com/ouqiang/gocron`
3. 编译
* 调度器 `go build -tags gocron -o gocron`
* 任务节点 `go build -tags node -o gocron-node`
4. 启动、访问方式同上
3. 编译 `make`
4. 启动
* gocron `./bin/gocron web`
* gocron-node `./bin/gocron-node`
### 命令
@@ -84,16 +88,15 @@
- [x] 批量开启、关闭、删除任务
- [x] 调度器与任务节点通信支持https
- [x] 任务分组
- [ ] 多用户
- [ ] 权限控制
- [ ] 新增任务API接口
- [x] 多用户
- [x] 权限控制
## 程序使用的组件
* 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)
* 依赖管理 [Govendor](https://github.com/kardianos/govendor)
* RPC框架 [gRPC](https://github.com/grpc/grpc)
## 反馈
@@ -101,6 +104,19 @@
## ChangeLog
v1.4
--------
* HTTP任务支持POST请求
* 后台手动停止运行中的shell任务
* 任务执行失败重试间隔时间支持用户自定义
* 修复API接口调用报403错误
v1.3
--------
* 支持多用户登录
* 增加用户权限控制
v1.2.2
--------
* 用户登录页增加图形验证码
@@ -109,8 +125,6 @@ v1.2.2
* 调度器与任务节点支持HTTPS双向认证
* 修复任务列表页总记录数显示错误
v1.1
--------
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env bash
# set -x -u
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
# ./build.sh -p windows -a amd64
# 参数含义
# -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64
TEMP_DIR=`date +%s`-temp-`echo $RANDOM`
# 目标平台 windows,linux,darwin
OS=''
# 目标平台架构
ARCH=''
# 应用名称
APP_NAME='gocron'
# 可执行文件名
EXEC_NAME=''
# 压缩包名称
COMPRESS_FILE=''
# -p 平台 -a 架构
while getopts "p:a:" OPT;
do
case $OPT in
p) OS=$OPTARG
;;
a) ARCH=$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
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
else
EXEC_NAME=${APP_NAME}
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz
fi
mkdir -p $TEMP_DIR/$APP_NAME
if [[ $? != 0 ]]; then
exit 1
fi
# 需要打包的文件
PACKAGE_FILENAME=(conf log public data templates ${EXEC_NAME})
echo '复制文件到临时目录'
# 复制文件到临时目录
for i in ${PACKAGE_FILENAME[*]}
do
cp -r $i $TEMP_DIR/$APP_NAME
done
# 删除运行时产生的文件
rm -rf $TEMP_DIR/$APP_NAME/conf/*
rm -rf $TEMP_DIR/$APP_NAME/log/*
rm -rf $TEMP_DIR/$APP_NAME/data/sessions/*
rm -rf $TEMP_DIR/$APP_NAME/data/ssh/password/*
rm -rf $TEMP_DIR/$APP_NAME/data/ssh/private_key/*
echo '压缩文件'
# 压缩文件
cd $TEMP_DIR
if [[ $OS = 'windows' ]];then
zip -rq $COMPRESS_FILE *
else
tar czf $COMPRESS_FILE *
fi
mv $COMPRESS_FILE ../
cd ../
rm $EXEC_NAME
rm -rf $TEMP_DIR
echo '打包完成'
echo '生成压缩文件--' $COMPRESS_FILE
-83
View File
@@ -1,83 +0,0 @@
#!/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'
# 可执行文件名
EXEC_NAME=''
# 压缩包名称
COMPRESS_FILE=''
# -p 平台 -a 架构
while getopts "p:a:" OPT;
do
case $OPT in
p) OS=$OPTARG
;;
a) ARCH=$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 [[ $OS = 'windows' ]];then
EXEC_NAME=${APP_NAME}.exe
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.zip
else
EXEC_NAME=${APP_NAME}
COMPRESS_FILE=${APP_NAME}-${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
+192
View File
@@ -0,0 +1,192 @@
// main gocron
package main
import (
"os"
"os/signal"
"syscall"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/routers"
"github.com/ouqiang/gocron/service"
"github.com/urfave/cli"
"gopkg.in/macaron.v1"
)
var AppVersion = "1.4"
// web服务器默认端口
const DefaultPort = 5920
func main() {
cliApp := cli.NewApp()
cliApp.Name = "gocron"
cliApp.Usage = "gocron service"
cliApp.Version = AppVersion
cliApp.Commands = getCommands()
cliApp.Flags = append(cliApp.Flags, []cli.Flag{}...)
cliApp.Run(os.Args)
}
// getCommands
func getCommands() []cli.Command {
command := cli.Command{
Name: "web",
Usage: "run web server",
Action: runWeb,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: "0.0.0.0",
Usage: "bind host",
},
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
Usage: "bind port",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
},
}
return []cli.Command{command}
}
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)
host := parseHost(ctx)
port := parsePort(ctx)
m.Run(host, port)
}
func initModule() {
if !app.Installed {
return
}
config, err := setting.Read(app.AppConfig)
if err != nil {
logger.Fatal("读取应用配置失败", err)
}
app.Setting = config
// 初始化DB
models.Db = models.CreateDb()
// 版本升级
upgradeIfNeed()
// 初始化定时任务
service.ServiceTask.Initialize()
}
// 解析端口
func parsePort(ctx *cli.Context) int {
port := DefaultPort
if ctx.IsSet("port") {
port = ctx.Int("port")
}
if port <= 0 || port >= 65535 {
port = DefaultPort
}
return port
}
func parseHost(ctx *cli.Context) string {
if ctx.IsSet("host") {
return ctx.String("host")
}
return "0.0.0.0"
}
func setEnvironment(ctx *cli.Context) {
env := "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 shutdown() {
defer func() {
logger.Info("已退出")
os.Exit(0)
}()
if !app.Installed {
return
}
logger.Info("应用准备退出")
// 停止所有任务调度
logger.Info("停止定时任务调度")
service.ServiceTask.WaitAndExit()
}
// 判断应用是否需要升级, 当存在版本号文件且版本小于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)
}
+68
View File
@@ -0,0 +1,68 @@
// main gocron-node
package main
import (
"flag"
"fmt"
"os"
"runtime"
"strings"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/ouqiang/gocron/modules/rpc/server"
"github.com/ouqiang/gocron/modules/utils"
)
var AppVersion = "1.4"
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)
}
-194
View File
@@ -1,194 +0,0 @@
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/modules/rpc/grpcpool"
)
// web服务器默认端口
const DefaultPort = 5920
var CmdWeb = cli.Command{
Name: "web",
Usage: "run web server",
Action: runWeb,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: "0.0.0.0",
Usage: "bind host",
},
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
Usage: "bind port",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
},
}
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)
host := parseHost(ctx)
port := parsePort(ctx)
m.Run(host, port)
}
func initModule() {
if !app.Installed {
return
}
config, err := setting.Read(app.AppConfig)
if err != nil {
logger.Fatal("读取应用配置失败", err)
}
app.Setting = config
// 初始化DB
models.Db = models.CreateDb()
// 版本升级
upgradeIfNeed()
// 初始化定时任务
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
}
return port
}
func parseHost(ctx *cli.Context) string {
if ctx.IsSet("host") {
return ctx.String("host")
}
return "0.0.0.0"
}
func setEnvironment(ctx *cli.Context) {
var env string = "prod"
if ctx.IsSet("env") {
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 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
-70
View File
@@ -1,70 +0,0 @@
// +build node
// 任务节点
package main
import (
"github.com/ouqiang/gocron/modules/rpc/server"
"flag"
"runtime"
"os"
"fmt"
"strings"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/ouqiang/gocron/modules/utils"
)
const AppVersion = "1.2.2"
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)
}
-25
View File
@@ -1,25 +0,0 @@
// +build gocron
// 调度中心
package main
import (
"github.com/urfave/cli"
"os"
"github.com/ouqiang/gocron/cmd"
)
const AppVersion = "1.2.2"
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)
}
View File
+18
View File
@@ -0,0 +1,18 @@
all: build
build: gocron node
gocron:
go build -o bin/gocron ./cmd/gocron
node:
go build -o bin/gocron-node ./cmd/node
clean:
rm bin/gocron
rm bin/gocron-node
+33
View File
@@ -0,0 +1,33 @@
LDFLAGS = -w -s -X main.AppVersion=${VERSION}
all: build
build: gocron node
gocron:
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_darwin_amd64/gocron ./cmd/gocron
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_linux_amd64/gocron ./cmd/gocron
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_windows_amd64/gocron.exe ./cmd/gocron
node:
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_darwin_amd64/gocron-node ./cmd/node
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_linux_amd64/gocron-node ./cmd/node
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_windows_amd64/gocron-node.exe ./cmd/node
clean:
rm -rf gocron_darwin_amd64
rm -rf gocron_linux_amd64
rm -rf gocron_windows_amd64
rm -rf gocron-node_darwin_amd64
rm -rf gocron-node_linux_amd64
rm -rf gocron-node_windows_amd64
+51 -52
View File
@@ -1,95 +1,94 @@
package models
import (
"github.com/go-xorm/xorm"
"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 '' "` // 主机别名
Port int `xorm:"notnull default 22"` // 主机端口
Remark string `xorm:"varchar(100) notnull default '' "` // 备注
BaseModel `xorm:"-"`
Selected bool `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,port,remark").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(new(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.Cols("name,port").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)
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)
}
+155 -120
View File
@@ -1,159 +1,194 @@
package models
import (
"errors"
"fmt"
"github.com/ouqiang/gocron/modules/logger"
"github.com/go-xorm/xorm"
"strconv"
)
"errors"
"fmt"
"strconv"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/logger"
)
type Migration struct{}
// 首次安装, 创建数据库表
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()
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
}
func (migration *Migration) Upgrade(oldVersionId int) {
// v1.2版本不支持升级
if oldVersionId == 120 {
return
}
versionIds := []int{110, 122}
upgradeFuncs := []func(*xorm.Session) error {
migration.upgradeFor110,
migration.upgradeFor122,
}
versionIds := []int{110, 122, 130, 140}
upgradeFuncs := []func(*xorm.Session) error{
migration.upgradeFor110,
migration.upgradeFor122,
migration.upgradeFor130,
migration.upgradeFor140,
}
startIndex := -1
// 从当前版本的下一版本开始升级
for i, value := range versionIds {
if value > oldVersionId {
startIndex = i
break;
}
}
startIndex := -1
// 从当前版本的下一版本开始升级
for i, value := range versionIds {
if value > oldVersionId {
startIndex = i
break
}
}
if startIndex == -1 {
return
}
if startIndex == -1 {
return
}
length := len(versionIds)
if startIndex >= length {
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())
}
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
}
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
}
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
}
}
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))
// 删除task表host_id字段
_, err = session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN host_id", tableName))
logger.Info("已升级到v1.1\n")
logger.Info("已升级到v1.1\n")
return err
return err
}
// 升级到1.2.2版本
func (migration *Migration) upgradeFor122(session *xorm.Session) error {
logger.Info("开始升级到v1.2.2")
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))
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")
logger.Info("已升级到v1.2.2\n")
return err
}
return err
}
// 升级到v1.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
}
// 升级到v1.4版本
func (migration *Migration) upgradeFor140(session *xorm.Session) error {
logger.Info("开始升级到v1.4")
tableName := TablePrefix + "task"
// task表增加字段
// retry_interval 重试间隔时间(秒)
// http_method http请求方法
sql := fmt.Sprintf(
"ALTER TABLE %s ADD COLUMN retry_interval SMALLINT NOT NULL DEFAULT 0,ADD COLUMN http_method TINYINT NOT NULL DEFAULT 1", tableName)
_, err := session.Exec(sql)
if err != nil {
return err
}
logger.Info("已升级到v1.4\n")
return err
}
+82 -82
View File
@@ -1,124 +1,124 @@
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"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/app"
"time"
"github.com/ouqiang/gocron/modules/setting"
"fmt"
"strings"
"time"
_ "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"
)
type Status int8
type CommonMap map[string]interface{}
var TablePrefix string = ""
var TablePrefix = ""
var Db *xorm.Engine
const (
Disabled Status = 0 // 禁用
Failure Status = 0 // 失败
Enabled Status = 1 // 启用
Running Status = 1 // 运行中
Finish Status = 2 // 完成
Cancel Status = 3 // 取消
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
}
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 {
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)
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)
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)
}
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)
}
go keepDbAlived(engine)
go keepDbAlived(engine)
return engine
return engine
}
// 创建临时数据库连接
func CreateTmpDb(setting *setting.Setting) (*xorm.Engine, error) {
dsn := getDbEngineDSN(setting)
func CreateTmpDb(setting *setting.Setting) (*xorm.Engine, error) {
dsn := getDbEngineDSN(setting)
return xorm.NewEngine(setting.Db.Engine, dsn)
return xorm.NewEngine(setting.Db.Engine, dsn)
}
// 获取数据库引擎DSN mysql,sqlite
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)
}
engine := strings.ToLower(setting.Db.Engine)
dsn := ""
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
}
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()
}
}
+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
+185 -172
View File
@@ -1,272 +1,285 @@
package models
import (
"time"
"github.com/go-xorm/xorm"
"errors"
"strings"
"errors"
"strings"
"time"
"github.com/go-xorm/xorm"
)
type TaskProtocol int8
const (
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
TaskRPC // RPC方式执行命令
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
TaskRPC // RPC方式执行命令
)
type TaskLevel int8
const (
TaskLevelParent TaskLevel = 1 // 父任务
TaskLevelChild TaskLevel = 2 // 子任务(依赖任务)
TaskLevelParent TaskLevel = 1 // 父任务
TaskLevelChild TaskLevel = 2 // 子任务(依赖任务)
)
type TaskDependencyStatus int8
const (
TaskDependencyStatusStrong TaskDependencyStatus = 1 // 强依赖
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
TaskDependencyStatusStrong TaskDependencyStatus = 1 // 强依赖
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
)
type TaskHTTPMethod int8
const (
TaskHTTPMethodGet TaskHTTPMethod = 1
TaskHttpMethodPost TaskHTTPMethod = 2
)
// 任务
type Task struct {
Id int `xorm:"int pk autoincr"`
Name string `xorm:"varchar(32) notnull"` // 任务名称
Level TaskLevel `xorm:"smallint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
DependencyStatus TaskDependencyStatus `xorm:"smallint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol TaskProtocol `xorm:"tinyint notnull 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:"-"`
Id int `xorm:"int pk autoincr"`
Name string `xorm:"varchar(32) notnull"` // 任务名称
Level TaskLevel `xorm:"tinyint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
DependencyStatus TaskDependencyStatus `xorm:"tinyint 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命令
HttpMethod TaskHTTPMethod `xorm:"tinyint notnull default 1"` // http请求方法
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
RetryInterval int16 `xorm:"smallint notnull default 0"` // 重试间隔时间
NotifyStatus int8 `xorm:"tinyint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
NotifyType int8 `xorm:"tinyint 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:"-"`
}
func taskHostTableName() []string {
return []string{TablePrefix + "task_host", "th"}
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.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()
// 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,remark,notify_status,notify_type,notify_receiver_id, dependency_task_id, dependency_status, tag").
Update(task)
func (task *Task) UpdateBean(id int) (int64, error) {
return Db.ID(id).
Cols(`name,spec,protocol,command,timeout,multi,
retry_times,retry_interval,remark,notify_status,
notify_type,notify_receiver_id, dependency_task_id, dependency_status, tag,http_method`).
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() ([]Task, error) {
list := make([]Task, 0)
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).
Find(&list)
func (task *Task) ActiveList(page, pageSize int) ([]Task, error) {
params := CommonMap{"Page": page, "PageSize": pageSize}
task.parsePageAndPageSize(params)
list := make([]Task, 0)
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).Limit(task.PageSize, task.pageLimitOffset()).
Find(&list)
if err != nil {
return list, err
}
if err != nil {
return list, err
}
return task.setHostsForTasks(list)
return task.setHostsForTasks(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
}
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 task.setHostsForTasks(list)
return task.setHostsForTasks(list)
}
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
}
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 tasks, 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) 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")
}
exist, err := Db.Id(id).Get(task)
if err != nil {
return 0, err
}
if !exist {
return 0, errors.New("not exist")
}
return task.Status, nil
return task.Status, nil
}
func(task *Task) Detail(id int) (Task, error) {
t := Task{}
_, err := Db.Where("id=?", id).Get(&t)
func (task *Task) Detail(id int) (Task, error) {
t := Task{}
_, err := Db.Where("id=?", id).Get(&t)
if err != nil {
return t, err
}
if err != nil {
return t, err
}
taskHostModel := new(TaskHost)
t.Hosts, err = taskHostModel.GetHostIdsByTaskId(id)
taskHostModel := new(TaskHost)
t.Hosts, err = taskHostModel.GetHostIdsByTaskId(id)
return t, err
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)
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
}
if err != nil {
return nil, err
}
return task.setHostsForTasks(list)
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)
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
}
if err != nil {
return list, err
}
return task.setHostsForTasks(list)
return task.setHostsForTasks(list)
}
func (task *Task) Total(params CommonMap) (int64, error) {
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
task.parseWhere(session, params)
list := make([]Task, 0)
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)
err := session.GroupBy("t.id").Find(&list)
return int64(len(list)), err
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("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 (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)
}
tag, ok := params["Tag"]
if ok && tag.(string) != "" {
session.And("tag = ? ", tag)
}
tag, ok := params["Tag"]
if ok && tag.(string) != "" {
session.And("tag = ? ", tag)
}
}
+45 -46
View File
@@ -1,81 +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"`
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
TaskHost `xorm:"extends"`
Name string
Port int
Alias string
}
func (TaskHostDetail) TableName() string {
return TablePrefix + "task_host"
func (TaskHostDetail) TableName() string {
return TablePrefix + "task_host"
}
func hostTableName() []string {
return []string{TablePrefix + "host", "h"}
return []string{TablePrefix + "host", "h"}
}
func (th *TaskHost) Remove(taskId int) error {
_, err := Db.Where("task_id = ?", taskId).Delete(new(TaskHost))
_, err := Db.Where("task_id = ?", taskId).Delete(new(TaskHost))
return err
return err
}
func (th *TaskHost) Add(taskId int, hostIds []int) error {
err := th.Remove(taskId)
if err != nil {
return err
}
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)
}
taskHosts := make([]TaskHost, len(hostIds))
for i, value := range hostIds {
taskHosts[i].TaskId = taskId
taskHosts[i].HostId = int16(value)
}
_, err = Db.Insert(&taskHosts)
_, err = Db.Insert(&taskHosts)
return err
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)
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
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
}
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
}
taskIds := make([]interface{}, len(list))
for i, value := range list {
taskIds[i] = value.TaskId
}
return taskIds, err
return taskIds, err
}
// 判断主机id是否有引用
func (th *TaskHost) HostIdExist(hostId int16) (bool, error) {
count, err := Db.Where("host_id = ?", hostId).Count(th);
count, err := Db.Where("host_id = ?", hostId).Count(th)
return count > 0, err
}
return count > 0, err
}
+65 -65
View File
@@ -1,99 +1,99 @@
package models
import (
"time"
"github.com/go-xorm/xorm"
"time"
"github.com/go-xorm/xorm"
)
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 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:"-"`
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)
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
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()
defer session.Close()
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)
}
}
+66 -50
View File
@@ -1,110 +1,126 @@
package models
import (
"github.com/ouqiang/gocron/modules/utils"
"time"
"time"
"github.com/ouqiang/gocron/modules/utils"
)
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)
}
+83 -78
View File
@@ -1,117 +1,122 @@
package app
import (
"os"
"os"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"io/ioutil"
"strconv"
"strings"
"github.com/ouqiang/gocron/modules/setting"
"fmt"
"io/ioutil"
"strconv"
"strings"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/utils"
)
var (
AppDir string // 应用根目录
ConfDir string // 配置目录
LogDir string // 日志目录
DataDir string // 存放session等
AppConfig string // 应用配置文件
Installed bool // 应用是否安装过
Setting *setting.Setting // 应用配置
VersionId int // 版本号
VersionFile string // 版本号文件
AppDir string // 应用根目录
ConfDir string // 配置目录
LogDir string // 日志目录
DataDir string // 存放session等
AppConfig string // 应用配置文件
Installed bool // 应用是否安装过
Setting *setting.Setting // 应用配置
VersionId int // 版本号
VersionFile string // 版本号文件
)
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)
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"
createDirIfNotExists(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,
)
func UpdateVersionFile() {
err := ioutil.WriteFile(VersionFile,
[]byte(strconv.Itoa(VersionId)),
0644,
)
if err != nil {
logger.Fatal(err)
}
if err != nil {
logger.Fatal(err)
}
}
// 获取应用当前版本号, 从版本号文件中读取
func GetCurrentVersionId() int {
if !utils.FileExist(VersionFile) {
return 0;
}
if !utils.FileExist(VersionFile) {
return 0
}
bytes, err := ioutil.ReadFile(VersionFile)
if err != nil {
logger.Fatal(err)
}
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)
}
versionId, err := strconv.Atoi(strings.TrimSpace(string(bytes)))
if err != nil {
logger.Fatal(err)
}
return versionId
return versionId
}
// 把字符串版本号a.b.c转换为整数版本号abc
func ToNumberVersion(versionString string) int {
v := strings.Replace(versionString, ".", "", -1)
if len(v) < 3 {
v += "0"
}
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)
}
versionId, err := strconv.Atoi(v)
if err != nil {
logger.Fatal(err)
}
return versionId
return versionId
}
// 检测目录是否存在
func checkDirExists(path ...string) {
for _, value := range path {
if !utils.FileExist(value) {
logger.Fatal(value + "目录不存在或无权限访问")
}
}
}
func createDirIfNotExists(path ...string) {
for _, value := range path {
if utils.FileExist(value) {
continue
}
err := os.Mkdir(value, 0755)
if err != nil {
logger.Fatal(fmt.Sprintf("创建目录失败:%s", err.Error()))
}
}
}
+55 -56
View File
@@ -3,81 +3,80 @@ 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("User-Agent", "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)}
}
+79 -78
View File
@@ -1,11 +1,12 @@
package logger
import (
"github.com/cihub/seelog"
"gopkg.in/macaron.v1"
"fmt"
"os"
"runtime"
"fmt"
"os"
"runtime"
"github.com/cihub/seelog"
"gopkg.in/macaron.v1"
)
// 日志库
@@ -22,113 +23,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 +146,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
}
+67 -68
View File
@@ -1,84 +1,83 @@
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"
"strconv"
"strings"
"time"
"github.com/go-gomail/gomail"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
)
// @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
}
+35 -34
View File
@@ -1,52 +1,53 @@
package notify
import (
"time"
"github.com/ouqiang/gocron/modules/logger"
"fmt"
"fmt"
"time"
"github.com/ouqiang/gocron/modules/logger"
)
type Message map[string]interface{}
type Notifiable interface {
Send(msg Message)
Send(msg Message)
}
var queue chan Message = make(chan Message, 100)
var queue = 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)
}
}
+64 -62
View File
@@ -1,78 +1,80 @@
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"
"strconv"
"strings"
"time"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
)
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)
}
+6 -7
View File
@@ -3,16 +3,17 @@ package auth
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"errors"
"fmt"
"io/ioutil"
"google.golang.org/grpc/credentials"
)
type Certificate struct {
CAFile string
CertFile string
KeyFile string
CAFile string
CertFile string
KeyFile string
ServerName string
}
@@ -33,14 +34,12 @@ func (c Certificate) GetTLSConfigForServer() (*tls.Config, error) {
return nil, errors.New("failed to append client certs")
}
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{certificate},
ClientCAs: certPool,
}
return tlsConfig, nil
}
@@ -68,4 +67,4 @@ func (c Certificate) GetTransportCredsForClient() (credentials.TransportCredenti
})
return transportCreds, nil
}
}
+71 -63
View File
@@ -1,80 +1,88 @@
package client
import (
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"golang.org/x/net/context"
"fmt"
"time"
"errors"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
"google.golang.org/grpc/codes"
"google.golang.org/grpc"
"github.com/ouqiang/gocron/modules/logger"
"errors"
"fmt"
"sync"
"time"
"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"
)
var (
errUnavailable = errors.New("无法连接远程服务器")
taskMap sync.Map
)
func ExecWithRetry(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
tryTimes := 60
i := 0
for i < tryTimes {
output, err := Exec(ip, port, taskReq)
if err != errUnavailable {
return output, err
}
i++
time.Sleep(2 * time.Second)
}
var (
errUnavailable = errors.New("无法连接远程服务器")
)
return "", errUnavailable
func generateTaskUniqueKey(ip string, port int, id int64) string {
return fmt.Sprintf("%s:%d:%d", ip, port, id)
}
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)
}
func Stop(ip string, port int, id int64) {
key := generateTaskUniqueKey(ip, port, id)
cancel, ok := taskMap.Load(key)
if !ok {
return
}
cancel.(context.CancelFunc)()
}
if resp.Error == "" {
return resp.Output, nil
}
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)
taskUniqueKey := generateTaskUniqueKey(ip, port, taskReq.Id)
taskMap.Store(taskUniqueKey, cancel)
defer taskMap.Delete(taskUniqueKey)
resp, err := c.Run(ctx, taskReq)
if err != nil {
return parseGRPCError(err, conn, &isConnClosed)
}
return resp.Output, errors.New(resp.Error)
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
switch grpc.Code(err) {
case codes.Unavailable, codes.Internal:
conn.Close()
*connClosed = true
return "", errUnavailable
case codes.DeadlineExceeded:
return "", errors.New("执行超时, 强制结束")
case codes.Canceled:
return "", errors.New("手动停止")
}
return "", err
}
+103 -104
View File
@@ -1,141 +1,140 @@
package grpcpool
import (
"github.com/silenceper/pool"
"sync"
"time"
"google.golang.org/grpc"
"errors"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/ouqiang/gocron/modules/app"
"strings"
)
"errors"
"strings"
"sync"
"time"
var (
Pool GRPCPool
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/silenceper/pool"
"google.golang.org/grpc"
)
var (
ErrInvalidConn = errors.New("invalid connection")
Pool GRPCPool
)
func init() {
Pool = GRPCPool{
make(map[string]pool.Pool),
sync.RWMutex{},
}
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
// 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
}
}
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
}
p.RLock()
pool = p.conns[addr]
p.RUnlock()
conn, err := pool.Get()
if err != nil {
return nil, err
}
return conn.(*grpc.ClientConn), nil
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)
}
p.RLock()
defer p.RUnlock()
pool, ok := p.conns[addr]
if ok {
return pool.Put(conn)
}
return ErrInvalidConn
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)
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) 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())
}
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, ":")
server := strings.Split(addr, ":")
certificate := auth.Certificate{
CAFile: app.Setting.CAFile,
CertFile: app.Setting.CertFile,
KeyFile: app.Setting.KeyFile,
ServerName: server[0],
}
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
}
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,
}
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
}
commonPool, err := pool.NewChannelPool(poolConfig)
if err != nil {
return err
}
p.conns[addr] = commonPool
p.conns[addr] = commonPool
return nil
}
return nil
}
+21 -12
View File
@@ -36,6 +36,7 @@ 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"`
Id int64 `protobuf:"varint,4,opt,name=id" json:"id,omitempty"`
}
func (m *TaskRequest) Reset() { *m = TaskRequest{} }
@@ -57,6 +58,13 @@ func (m *TaskRequest) GetTimeout() int32 {
return 0
}
func (m *TaskRequest) GetId() int64 {
if m != nil {
return m.Id
}
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"`
@@ -161,16 +169,17 @@ var _Task_serviceDesc = grpc.ServiceDesc{
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,
// 184 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xcf, 0x8a, 0x83, 0x30,
0x10, 0xc6, 0x37, 0x46, 0x5d, 0x76, 0x76, 0x59, 0xda, 0xa1, 0x94, 0xd0, 0x93, 0x78, 0xf2, 0x50,
0x3c, 0xb4, 0x3d, 0xf6, 0x25, 0x1a, 0xfa, 0x02, 0x56, 0x73, 0x10, 0xd1, 0x49, 0x93, 0xc9, 0xfb,
0x17, 0xff, 0x81, 0xc7, 0xdf, 0x0c, 0xf3, 0xfd, 0xe6, 0x03, 0xe0, 0xca, 0x77, 0xa5, 0x75, 0xc4,
0x84, 0xd2, 0xd9, 0x3a, 0x7f, 0xc0, 0xef, 0xb3, 0xf2, 0x9d, 0x36, 0xef, 0x60, 0x3c, 0xa3, 0x82,
0xef, 0x9a, 0xfa, 0xbe, 0x1a, 0x1a, 0x15, 0x65, 0xa2, 0xf8, 0xd1, 0x2b, 0x8e, 0x1b, 0x6e, 0x7b,
0x43, 0x81, 0x95, 0xcc, 0x44, 0x91, 0xe8, 0x15, 0xf1, 0x1f, 0xa2, 0xb6, 0x51, 0x71, 0x26, 0x0a,
0xa9, 0xa3, 0xb6, 0xc9, 0xef, 0xf0, 0x37, 0x47, 0x7a, 0x4b, 0x83, 0x37, 0x78, 0x84, 0x94, 0x02,
0xdb, 0xc0, 0x4a, 0x4c, 0x91, 0x0b, 0xe1, 0x01, 0x12, 0xe3, 0x1c, 0xb9, 0xc5, 0x34, 0xc3, 0xe5,
0x06, 0xf1, 0x78, 0x8d, 0x67, 0x90, 0x3a, 0x0c, 0xb8, 0x2b, 0x9d, 0xad, 0xcb, 0xcd, 0x8b, 0xa7,
0xfd, 0x66, 0x32, 0x1b, 0xf2, 0xaf, 0x57, 0x3a, 0x55, 0xba, 0x7e, 0x02, 0x00, 0x00, 0xff, 0xff,
0xd7, 0x7f, 0x8a, 0x9d, 0xe0, 0x00, 0x00, 0x00,
}
+1
View File
@@ -9,6 +9,7 @@ service Task {
message TaskRequest {
string command = 2; // 命令
int32 timeout = 3; // 任务执行超时时间
int64 id = 4; // 执行任务唯一ID
}
message TaskResponse {
+47 -53
View File
@@ -1,65 +1,59 @@
package server
import (
"golang.org/x/net/context"
"net"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/rpc/auth"
"google.golang.org/grpc/credentials"
"net"
"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"
)
type Server struct {}
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 = ""
}
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
return resp, nil
}
func Start(addr string, enableTLS bool, certificate auth.Certificate) {
defer func() {
if err := recover(); err != nil {
grpclog.Println("panic", err)
}
} ()
func Start(addr string, enableTLS bool, certificate auth.Certificate) {
l, err := net.Listen("tcp", addr)
if err != nil {
grpclog.Fatal(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)
}
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)
err = s.Serve(l)
grpclog.Fatal(err)
}
+87 -83
View File
@@ -1,111 +1,115 @@
package setting
import (
"errors"
"gopkg.in/ini.v1"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/logger"
"errors"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/ini.v1"
)
const DefaultSection = "default"
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
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
EnableTLS bool
CAFile string
CertFile string
KeyFile string
EnableTLS bool
CAFile string
CertFile string
KeyFile string
ConcurrencyQueue int
}
// 读取配置
func Read(filename string) (*Setting,error) {
config, err := ini.Load(filename)
if err != nil {
return nil, err
}
section := config.Section(DefaultSection)
func Read(filename string) (*Setting, error) {
config, err := ini.Load(filename)
if err != nil {
return nil, err
}
section := config.Section(DefaultSection)
var s Setting
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.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.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.ConcurrencyQueue = section.Key("concurrency.queue").MustInt(500)
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("")
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 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.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)
}
}
if !utils.FileExist(s.KeyFile) {
logger.Fatalf("failed to read client key file: %s", s.KeyFile)
}
}
return &s, nil
return &s, nil
}
// 写入配置
func Write(config []string, filename string) error {
if len(config) == 0 {
return errors.New("参数不能为空")
}
if len(config) % 2 != 0 {
return errors.New("参数不匹配")
}
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 i := 0 ;i < len(config); {
_, err = section.NewKey(config[i], config[i+1])
if err != nil {
return err
}
i += 2
}
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
}
-143
View File
@@ -1,143 +0,0 @@
package ssh
import (
"golang.org/x/crypto/ssh"
"fmt"
"net"
"time"
"errors"
)
type HostAuthType int8 // 认证方式
const (
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 Result struct {
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
},
}
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
},
}
return
}
// 执行shell命令
func Exec(sshConfig SSHConfig, cmd string) (output string, err error) {
client, err := getClient(sshConfig)
if err != nil {
return "", err
}
defer client.Close()
session, err := client.NewSession()
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
}
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
}
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)
}
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,17 @@
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{}
@@ -26,40 +27,40 @@ 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)
}
+54 -65
View File
@@ -1,107 +1,96 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"math/rand"
"time"
"runtime"
"github.com/Tang-RoseChild/mahonia"
"strings"
"os"
"fmt"
"crypto/md5"
"encoding/hex"
"math/rand"
"os"
"strings"
"time"
"github.com/Tang-RoseChild/mahonia"
)
// 生成长度为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)
}
// 判断当前系统是否是windows
func IsWindows() bool {
return runtime.GOOS == "windows"
return r.Intn(max)
}
// 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)
}
+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)
}
}
+27 -26
View File
@@ -3,35 +3,36 @@
package utils
import (
"os/exec"
"syscall"
"golang.org/x/net/context"
"errors"
"errors"
"os/exec"
"syscall"
"golang.org/x/net/context"
)
type Result struct {
output string
err error
output string
err error
}
// 执行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
}
}
func ExecShell(ctx context.Context, command string) (string, error) {
cmd := exec.Command("/bin/bash", "-c", command)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
resultChan := 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
}
}
+37 -39
View File
@@ -3,51 +3,49 @@
package utils
import (
"syscall"
"os/exec"
"strconv"
"golang.org/x/net/context"
"errors"
"errors"
"os/exec"
"strconv"
"syscall"
"golang.org/x/net/context"
)
type Result struct {
output string
err error
output string
err error
}
// 执行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
}
return "", nil
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
}
func ConvertEncoding(outputGBK string) string {
// windows平台编码为gbk,需转换为utf8才能入库
outputUTF8, ok := GBK2UTF8(outputGBK)
if ok {
return outputUTF8
}
return "命令输出转换编码失败(gbk to utf8)"
}
return "命令输出转换编码失败(gbk to utf8)"
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
#set -x -u
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
# ./package.sh -v 1.4
VERSION=''
GOCRON_APP_NAME='gocron'
GOCRON_NODE_APP_NAME='gocron-node'
PACKAGE_DIR='./packages'
# 用法
usage() {
echo 'usage: ./package.sh -v version'
}
# 初始化
init() {
rm -rf ${PACKAGE_DIR}
mkdir -p ${PACKAGE_DIR}
}
# 构建应用
build() {
make -f makefile.cross-compiles VERSION=${VERSION}
if [[ $? -ne 0 ]];then
echo 'make error'
exit 1
fi
}
# 清理
clean() {
make -f makefile.cross-compiles clean
}
# 打包gocron
package_gocron() {
local OS=$1
local GOCRON_COMPRESS_FILE=''
local PLATFORM_NAME=${GOCRON_APP_NAME}_${OS}_amd64
if [[ ! -d ${PACKAGE_DIR}/${PLATFORM_NAME} ]];then
mkdir ${PACKAGE_DIR}/${PLATFORM_NAME}
fi
for file in public templates LICENSE README.md Dockerfile-release; do
cp -r ${file} ${PLATFORM_NAME}
done
if [[ ${OS} = 'windows' ]];then
GOCRON_COMPRESS_FILE=${GOCRON_APP_NAME}-v${VERSION}-${OS}-amd64.zip
zip -rq ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_COMPRESS_FILE} ${PLATFORM_NAME}
else
GOCRON_COMPRESS_FILE=${GOCRON_APP_NAME}-v${VERSION}-${OS}-amd64.tar.gz
tar czf ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_COMPRESS_FILE} ${PLATFORM_NAME}
fi
}
# 打包gocron-node
package_gocron_node() {
local OS=$1
local GOCRON_NODE_COMPRESS_FILE=''
local PLATFORM_NAME=${GOCRON_NODE_APP_NAME}_${OS}_amd64
if [[ ! -d ${PACKAGE_DIR}/${PLATFORM_NAME} ]];then
mkdir ${PACKAGE_DIR}/${PLATFORM_NAME}
fi
if [[ ${OS} = 'windows' ]];then
GOCRON_NODE_COMPRESS_FILE=${GOCRON_NODE_APP_NAME}-v${VERSION}-${OS}-amd64.zip
zip -rq ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_NODE_COMPRESS_FILE} ${PLATFORM_NAME}
else
GOCRON_NODE_COMPRESS_FILE=${GOCRON_NODE_APP_NAME}-v${VERSION}-${OS}-amd64.tar.gz
tar czf ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_NODE_COMPRESS_FILE} ${PLATFORM_NAME}
fi
}
package_multi() {
for os in darwin linux windows; do
package_gocron ${os}
package_gocron_node ${os}
done
}
while getopts "v:" OPT; do
case ${OPT} in
v) VERSION=${OPTARG}
;;
esac
done
if [[ -z ${VERSION} ]]; then
usage
exit 1
fi
run() {
init
build
package_multi
clean
}
run
+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
}
+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")
}
+162 -157
View File
@@ -1,194 +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/service"
"github.com/Unknwon/paginater"
"fmt"
"html/template"
"github.com/ouqiang/gocron/routers/base"
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
"strings"
"github.com/ouqiang/gocron/modules/rpc/client"
"github.com/ouqiang/gocron/modules/rpc/proto"
"fmt"
"html/template"
"strconv"
"strings"
"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"
)
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")
// 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)"`
Port int `binding:"Required;Range(1-65535)"`
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 = 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("主机不存在")
}
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("主机不存在")
}
if id > 0 {
_, err = hostModel.UpdateBean(id)
} else {
isCreate = true
id, err = hostModel.Create()
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
if id > 0 {
_, err = hostModel.UpdateBean(id)
} else {
isCreate = true
id, err = hostModel.Create()
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
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)
}
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)
}
taskModel := new(models.Task)
tasks, err := taskModel.ActiveListByHostId(id)
if err != nil {
return json.CommonFailure("刷新任务主机信息失败", err)
}
service.ServiceTask.BatchAdd(tasks)
}
return json.Success("保存成功", nil)
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)
}
taskHostModel := new(models.TaskHost)
exist,err := taskHostModel.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.Find(int(id))
if err != nil {
return json.CommonFailure("主机不存在")
}
hostModel := new(models.Host)
err = hostModel.Find(int(id))
if err != nil {
return json.CommonFailure("主机不存在")
}
_, err =hostModel.Delete(id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
_, err = hostModel.Delete(id)
if err != nil {
return json.CommonFailure("操作失败", err)
}
addr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
grpcpool.Pool.Release(addr)
addr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
grpcpool.Pool.Release(addr)
return json.Success("操作成功", nil)
return json.Success("操作成功", nil)
}
func Ping(ctx *macaron.Context) string {
id := ctx.ParamsInt(":id")
hostModel := new(models.Host)
err := hostModel.Find(id)
json := utils.JsonResponse{}
if err != nil || hostModel.Id <= 0{
return json.CommonFailure("主机不存在", err)
}
// 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)
}
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)
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{}
params["Id"] = ctx.QueryInt("id")
params["Name"] = ctx.QueryTrim("name")
base.ParsePageAndPageSize(ctx, params)
return params
}
return params
}
+121 -120
View File
@@ -1,163 +1,164 @@
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"
"strconv"
"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"
)
// 系统安装
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.Install(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.UpdateVersionFile()
// 更新版本号文件
app.UpdateVersionFile()
app.Installed = true
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
app.Installed = true
// 初始化定时任务
service.ServiceTask.Initialize()
return json.Success("安装成功", nil)
return json.Success("安装成功", nil)
}
// 配置写入文件
func writeConfig(form InstallForm) error {
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", "",
}
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",
"concurrency.queue", "500",
"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 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
}
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
}
}
+26 -25
View File
@@ -1,30 +1,31 @@
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"
"html/template"
"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"
)
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")
}
+84 -85
View File
@@ -1,138 +1,137 @@
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
+279 -243
View File
@@ -1,27 +1,29 @@
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"
"time"
"strconv"
"html/template"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
"html/template"
"net/http"
"strconv"
"strings"
"time"
"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"
)
// 静态文件目录
@@ -29,261 +31,295 @@ 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("/log/stop", tasklog.Stop)
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("/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.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("/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.Post("/tasklog/remove/:id", tasklog.Remove)
m.Post("/task/enable/:id", task.Enable)
m.Post("/task/disable/:id", task.Disable)
}, apiAuth)
// 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(http.StatusNotFound, "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(http.StatusInternalServerError, "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,
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)
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{
// 模板语法分隔符,默认为 ["{{", "}}"]
Delims: macaron.Delims{"{%", "%}"},
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.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 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(http.StatusForbidden)
}
}
// 用户认证
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
}
uri := strings.TrimSpace(ctx.Req.URL.Path)
uri = strings.TrimRight(uri, "/")
if strings.HasPrefix(uri, "/api") {
return
}
// 普通用户允许访问的URL地址
allowPaths := []string{
"",
"/task",
"/task/log",
"/host",
"/user/login",
"/user/logout",
"/user/editMyPassword",
}
for _, path := range allowPaths {
if path == uri {
return
}
}
ctx.Status(http.StatusUnauthorized)
}
/** 设置共享数据 **/
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.AppName
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
}
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 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"
}
+267 -261
View File
@@ -1,335 +1,341 @@
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"
"html/template"
"strconv"
"strings"
"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"
)
type TaskForm struct {
Id int
Level models.TaskLevel `binding:"Required;In(1,2)"`
DependencyStatus models.TaskDependencyStatus
DependencyTaskId string
Name string `binding:"Required;MaxSize(32)"`
Spec string
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
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)"`
HttpMethod models.TaskHTTPMethod `binding:"In(1,2)"`
Timeout int `binding:"Range(0,86400)"`
Multi int8 `binding:"In(1,2)"`
RetryTimes int8
RetryInterval int16
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&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 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")
}
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
}
}
}
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")
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
nameExists, err := taskModel.NameExist(form.Name, form.Id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
}
if nameExists {
return json.CommonFailure("任务名称已存在")
}
// 保存任务 todo 拆分为多个方法 快变成意大利面条式代码了
func Store(ctx *macaron.Context, form TaskForm) string {
json := utils.JsonResponse{}
taskModel := models.Task{}
var id = 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.TaskRPC && form.HostId == "" {
return json.CommonFailure("请选择主机名")
}
if form.Protocol == models.TaskRPC && form.HostId == "" {
return json.CommonFailure("请选择主机名")
}
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秒")
}
}
taskModel.Name = form.Name
taskModel.Protocol = form.Protocol
taskModel.Command = strings.TrimSpace(form.Command)
taskModel.Timeout = form.Timeout
taskModel.Tag = form.Tag
taskModel.Remark = form.Remark
taskModel.Multi = form.Multi
taskModel.RetryTimes = form.RetryTimes
taskModel.RetryInterval = form.RetryInterval
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("至少选择一个通知接收者")
}
taskModel.HttpMethod = form.HttpMethod
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.RetryInterval > 3600 || taskModel.RetryInterval < 0 {
return json.CommonFailure("任务重试间隔时间取值0-3600")
}
if taskModel.Level == models.TaskLevelParent {
_, err = cron.Parse(form.Spec)
if err != nil {
return json.CommonFailure("crontab表达式解析失败", err)
}
} else {
taskModel.DependencyTaskId = ""
taskModel.Spec = ""
}
if taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
taskModel.DependencyStatus != models.TaskDependencyStatusWeak {
return json.CommonFailure("请选择依赖关系")
}
if id > 0 && taskModel.DependencyTaskId != "" {
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
return json.CommonFailure("不允许设置当前任务为子任务")
}
}
if taskModel.Level == models.TaskLevelParent {
_, err = cron.Parse(form.Spec)
if err != nil {
return json.CommonFailure("crontab表达式解析失败", err)
}
} else {
taskModel.DependencyTaskId = ""
taskModel.Spec = ""
}
if id == 0 {
// 任务添加后开始调度执行
taskModel.Status = models.Running
id, err = taskModel.Create()
} else {
_, err = taskModel.UpdateBean(id)
}
if id > 0 && taskModel.DependencyTaskId != "" {
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
return json.CommonFailure("不允许设置当前任务为子任务")
}
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
if id == 0 {
// 任务添加后开始调度执行
taskModel.Status = models.Running
id, err = taskModel.Create()
} else {
_, err = taskModel.UpdateBean(id)
}
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)
}
if err != nil {
return json.CommonFailure("保存失败", err)
}
status, err := taskModel.GetStatus(id)
if status == models.Enabled && taskModel.Level == models.TaskLevelParent {
addTaskToTimer(id)
}
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)
}
return json.Success("保存成功", nil)
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)
}
taskHostModel := new(models.TaskHost)
taskHostModel.Remove(id)
taskHostModel := new(models.TaskHost)
taskHostModel.Remove(id)
service.Cron.RemoveJob(strconv.Itoa(id))
service.ServiceTask.Remove(id)
return json.Success(utils.SuccessContent, nil)
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 = "手动运行"
service.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.ServiceTask.Remove(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)
service.ServiceTask.RemoveAndAdd(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")
params["Tag"] = ctx.QueryTrim("tag")
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)
hostModel.PageSize = -1
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
}
}
for _, v := range slice {
if v.HostId == element {
return true
}
}
return false
}
return false
}
+86 -60
View File
@@ -3,78 +3,104 @@ 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"
"fmt"
"html/template"
"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"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
)
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)
}
// 停止运行中的任务
func Stop(ctx *macaron.Context) string {
id := ctx.QueryInt64("id")
taskId := ctx.QueryInt("task_id")
taskModel := new(models.Task)
task, err := taskModel.Detail(taskId)
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("获取任务信息失败#"+err.Error(), err)
}
if task.Protocol != models.TaskRPC {
return json.CommonFailure("仅支持SHELL任务手动停止")
}
if len(task.Hosts) == 0 {
return json.CommonFailure("任务节点列表为空")
}
for _, host := range task.Hosts {
service.ServiceTask.Stop(host.Name, host.Port, id)
}
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")
}
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)
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
}
+296 -92
View File
@@ -1,127 +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"
"fmt"
"html/template"
"strings"
"github.com/Unknwon/paginater"
"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"
)
// @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
}
// 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, "验证码错误")
}
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)
}
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)
sess.Set("username", userModel.Name)
sess.Set("uid", userModel.Id)
sess.Set("isAdmin", userModel.IsAdmin)
return json.Success("登录成功", nil)
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
}

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

+366 -309
View File
@@ -1,414 +1,471 @@
package service
import (
"github.com/ouqiang/gocron/models"
"strconv"
"time"
"github.com/ouqiang/gocron/modules/logger"
"github.com/jakecoffman/cron"
"errors"
"fmt"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/notify"
"sync"
rpcClient "github.com/ouqiang/gocron/modules/rpc/client"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"strings"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/jakecoffman/cron"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"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"
)
// 定时任务调度管理器
var Cron *cron.Cron
// 同一任务是否有实例处于运行中
var runInstance Instance
// 任务计数-正在运行中的任务
var TaskNum TaskCount
var (
ServiceTask Task
)
var (
// 定时任务调度管理器
serviceCron *cron.Cron
// 同一任务是否有实例处于运行中
runInstance Instance
// 任务计数-正在运行的任务
taskCount TaskCount
// 并发队列, 限制同时运行的任务数量
concurrencyQueue ConcurrencyQueue
)
// 并发队列
type ConcurrencyQueue struct {
queue chan struct{}
}
func (cq *ConcurrencyQueue) Add() {
cq.queue <- struct{}{}
}
func (cq *ConcurrencyQueue) Done() {
<-cq.queue
}
// 任务计数
type TaskCount struct {
num int
sync.RWMutex
wg sync.WaitGroup
exit chan struct{}
}
func (c *TaskCount) Add() {
c.Lock()
defer c.Unlock()
c.num += 1
func (tc *TaskCount) Add() {
tc.wg.Add(1)
}
func (c *TaskCount) Done() {
c.Lock()
defer c.Unlock()
c.num -= 1
func (tc *TaskCount) Done() {
tc.wg.Done()
}
func (c *TaskCount) Num() int {
c.RLock()
defer c.RUnlock()
func (tc *TaskCount) Exit() {
tc.wg.Done()
<-tc.exit
}
return c.num
func (tc *TaskCount) Wait() {
tc.Add()
tc.wg.Wait()
close(tc.exit)
}
// 任务ID作为Key
type Instance struct {
Status map[int]bool
sync.RWMutex
m sync.Map
}
// 是否有任务处于运行中
func (i *Instance) has(key int) bool {
i.RLock()
defer i.RUnlock()
running, ok := i.Status[key]
if ok && running {
return true
}
_, ok := i.m.Load(key)
return false
return ok
}
func (i *Instance) add(key int) {
i.Lock()
defer i.Unlock()
i.Status[key] = true
func (i *Instance) add(key int) {
i.m.Store(key, struct{}{})
}
func (i *Instance) done(key int) {
i.Lock()
defer i.Unlock()
delete(i.Status, key)
func (i *Instance) done(key int) {
i.m.Delete(key)
}
type Task struct{}
type TaskResult struct {
Result string
Err error
RetryTimes int8
Result string
Err error
RetryTimes int8
}
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
func (task *Task) Initialize() {
Cron = cron.New()
Cron.Start()
runInstance = Instance{make(map[int]bool), sync.RWMutex{}}
TaskNum = TaskCount{0, sync.RWMutex{}}
func (task Task) Initialize() {
serviceCron = cron.New()
serviceCron.Start()
concurrencyQueue = ConcurrencyQueue{queue: make(chan struct{}, app.Setting.ConcurrencyQueue)}
taskCount = TaskCount{sync.WaitGroup{}, make(chan struct{})}
go taskCount.Wait()
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)
logger.Info("开始初始化定时任务")
taskModel := new(models.Task)
taskNum := 0
page := 1
pageSize := 1000
maxPage := 1000
for page < maxPage {
taskList, err := taskModel.ActiveList(page, pageSize)
if err != nil {
logger.Fatalf("定时任务初始化#获取任务列表错误-", err.Error())
}
if len(taskList) == 0 {
break
}
for _, item := range taskList {
task.Add(item)
taskNum++
}
page++
}
logger.Infof("定时任务初始化完成, 共%d个定时任务添加到调度器", taskNum)
}
// 批量添加任务
func (task *Task) BatchAdd(tasks []models.Task) {
for _, item := range tasks {
task.Add(item)
}
func (task Task) BatchAdd(tasks []models.Task) {
for _, item := range tasks {
task.RemoveAndAdd(item)
}
}
// 删除任务后添加
func (task Task) RemoveAndAdd(taskModel models.Task) {
task.Remove(taskModel.Id)
task.Add(taskModel)
}
// 添加任务
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
}
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)
err := serviceCron.AddFunc(taskModel.Spec, taskFunc, cronName)
if err != nil {
logger.Error("添加任务到调度器失败#", err)
}
}
// 停止所有任务
func (task *Task) StopAll() {
Cron.Stop()
// 停止运行中的任务
func (task Task) Stop(ip string, port int, id int64) {
rpcClient.Stop(ip, port, id)
}
func (task Task) Remove(id int) {
serviceCron.RemoveJob(strconv.Itoa(id))
}
// 等待所有任务结束后退出
func (task Task) WaitAndExit() {
serviceCron.Stop()
taskCount.Exit()
}
// 直接运行任务
func (task *Task) Run(taskModel models.Task) {
go createJob(taskModel)()
func (task Task) Run(taskModel models.Task) {
go createJob(taskModel)()
}
type Handler interface {
Run(taskModel models.Task) (string, error)
Run(taskModel models.Task, taskUniqueId int64) (string, error)
}
// HTTP任务
type HTTPHandler struct{}
// http任务执行时间不超过300秒
const HttpExecTimeout = 300
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))
}
func (h *HTTPHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout
}
var resp httpclient.ResponseWrapper
if taskModel.HttpMethod == models.TaskHTTPMethodGet {
resp = httpclient.Get(taskModel.Command, taskModel.Timeout)
} else {
urlFields := strings.Split(taskModel.Command, "?")
taskModel.Command = urlFields[0]
var params string
if len(urlFields) >= 2 {
params = urlFields[1]
}
resp = httpclient.PostParams(taskModel.Command, params, taskModel.Timeout)
}
// 返回状态码非200,均为失败
if resp.StatusCode != http.StatusOK {
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
}
return resp.Body, err
return resp.Body, err
}
// RPC调用执行任务
type RPCHandler struct {}
type RPCHandler struct{}
func (h *RPCHandler) Run(taskModel models.Task) (result string, err error) {
taskRequest := new(pb.TaskRequest)
taskRequest.Timeout = int32(taskModel.Timeout)
taskRequest.Command = taskModel.Command
var resultChan chan TaskResult = make(chan TaskResult, len(taskModel.Hosts))
for _, taskHost := range taskModel.Hosts {
go func(th models.TaskHostDetail) {
output, err := rpcClient.ExecWithRetry(th.Name, th.Port, 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)
}
func (h *RPCHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
taskRequest := new(pb.TaskRequest)
taskRequest.Timeout = int32(taskModel.Timeout)
taskRequest.Command = taskModel.Command
taskRequest.Id = taskUniqueId
resultChan := 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)
errorMessage := ""
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)
}
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
}
}
var aggregationErr error = nil
aggregationResult := ""
for i := 0; i < len(taskModel.Hosts); i++ {
taskResult := <-resultChan
aggregationResult += taskResult.Result
if taskResult.Err != nil {
aggregationErr = taskResult.Err
}
}
return aggregationResult, aggregationErr
return aggregationResult, aggregationErr
}
// 创建任务日志
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()
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 {
aggregationHost := ""
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, 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 {
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
result := 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.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)
}
handler := createHandler(taskModel)
if handler == nil {
return nil
}
taskFunc := func() {
taskCount.Add()
defer taskCount.Done()
return taskFunc
taskLogId := beforeExecJob(taskModel)
if taskLogId <= 0 {
return
}
if taskModel.Multi == 0 {
runInstance.add(taskModel.Id)
defer runInstance.done(taskModel.Id)
}
concurrencyQueue.Add()
defer concurrencyQueue.Done()
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command)
taskResult := execJob(handler, taskModel, taskLogId)
logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId)
}
return taskFunc
}
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)
}
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.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
}
func beforeExecJob(taskModel models.Task) (taskLogId int64) {
if taskModel.Multi == 0 && runInstance.has(taskModel.Id) {
createTaskLog(taskModel, models.Cancel)
return
}
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.Task, taskResult TaskResult, taskLogId int64) {
_, err := updateTaskLog(taskLogId, taskResult)
if err != nil {
logger.Error("任务结束#更新任务日志失败-", err)
}
func afterExecJob(taskModel models.Task, taskResult TaskResult, taskLogId int64) {
_, err := updateTaskLog(taskLogId, taskResult)
if err != nil {
logger.Error("任务结束#更新任务日志失败-", err)
}
// 发送邮件
go SendNotification(taskModel, taskResult)
// 执行依赖任务
go execDependencyTask(taskModel, taskResult)
// 发送邮件
go SendNotification(taskModel, taskResult)
// 执行依赖任务
go execDependencyTask(taskModel, taskResult)
}
// 执行依赖任务, 多个任务并发执行
func execDependencyTask(taskModel models.Task, taskResult TaskResult) {
// 父任务才能执行子任务
if taskModel.Level != models.TaskLevelParent {
return
}
func execDependencyTask(taskModel models.Task, taskResult TaskResult) {
// 父任务才能执行子任务
if taskModel.Level != models.TaskLevelParent {
return
}
// 是否存在子任务
dependencyTaskId := strings.TrimSpace(taskModel.DependencyTaskId)
if dependencyTaskId == "" {
return
}
// 是否存在子任务
dependencyTaskId := strings.TrimSpace(taskModel.DependencyTaskId)
if dependencyTaskId == "" {
return
}
// 父子任务关系为强依赖, 父任务执行失败, 不执行依赖任务
if taskModel.DependencyStatus == models.TaskDependencyStatusStrong && taskResult.Err != nil {
logger.Infof("父子任务为强依赖关系, 父任务执行失败, 不运行依赖任务#主任务ID-%d", taskModel.Id)
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)
}
// 获取子任务
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)
}
for _, task := range tasks {
task.Spec = fmt.Sprintf("依赖任务(主任务ID-%d)", taskModel.Id)
ServiceTask.Run(task)
}
}
// 发送任务结果通知
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 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.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)
}
}
func execJob(handler Handler, taskModel models.Task, taskUniqueId int64) TaskResult {
defer func() {
if err := recover(); err != nil {
logger.Error("panic#service/task.go:execJob#", err)
}
}()
// 默认只运行任务一次
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, taskUniqueId)
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())
if taskModel.RetryInterval > 0 {
time.Sleep(time.Duration(taskModel.RetryInterval) * time.Second)
} else {
// 默认重试间隔时间,每次递增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}
}
+15 -15
View File
@@ -4,7 +4,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="renderer" content="webkit">
<title>{{{.Title}}}</title>
<title>{%.Title%}</title>
<link type="text/css" rel="stylesheet" href="/resource/css/framework.css" />
<link type="text/css" rel="stylesheet" href="/resource/css/main.css" />
<link type="text/css" rel="stylesheet" href="/resource/sweetalert/sweetalert.css" />
@@ -29,29 +29,29 @@
</head>
<body>
<div class="page">
{{{if not .DisableNav}}}
{%if not .DisableNav%}
<!--header begin-->
<header>
<div class="bigcontainer">
<div id="logo">
{{{.AppName}}}
{%.AppName%}
</div>
<div class="user">
<div class="ui inline labeled icon top right pointing dropdown">
{{{if not .LoginUsername}}}
{%if not .LoginUsername%}
你好
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="/user/login"><i class="sign in icon"></i>登录</a>
</div>
{{{else}}}
你好{{{.LoginUsername}}}
{%else%}
你好{%.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}}}
{%end%}
</div>
</div>
</div>
@@ -60,13 +60,13 @@
<div class="ui teal inverted menu">
<div class="bigcontainer">
<div class="right menu">
<a class="item {{{if or (eq .Controller "task") (eq .Controller "delaytask")}}}active{{{end}}}" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item {{{if eq .Controller "host"}}}active{{{end}}}" href="/host"><i class="linux icon"></i>任务节点</a>
<!-- <a class="item {{{if eq .Controller "user"}}}active{{{end}}}" href="/user"><i class="user icon"></i>账户</a> -->
{{{if gt .LoginUid 0}}}
<a class="item {{{if eq .Controller "manage"}}}active{{{end}}}" href="/manage/slack/edit"><i class="settings icon"></i>管理</a>
{{{end}}}
<a class="item {%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%}
</div>
</div>
</div>
{{{end}}}
{%end%}
+22 -22
View File
@@ -1,41 +1,41 @@
{{{if gt .Pagination.Total 0}}}
{%if gt .Pagination.Total 0%}
<div class="ui pagination menu">
{{{if not .Pagination.IsFirst}}}
<a class="item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page=1">
{%if not .Pagination.IsFirst%}
<a class="item" href="{%.URI%}?{% .Params.PageParams%}&page=1">
首页
</a>
{{{end}}}
{%end%}
{{{if .Pagination.HasPrevious}}}
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.Previous}}}">
{%if .Pagination.HasPrevious%}
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.Previous%}">
<i class="icon left arrow"></i>
</a>
{{{end}}}
{%end%}
{{{range .Pagination.Pages}}}
{{{if eq .Num -1}}}
{%range .Pagination.Pages%}
{%if eq .Num -1%}
<div class="disabled item">
...
</div>
{{{else}}}
<a class="icon {{{if .IsCurrent}}}active{{{end}}} item" href="{{{$.URI}}}?{{{ $.Params.PageParams}}}&page={{{.Num}}}">
{{{.Num}}}
{%else%}
<a class="icon {%if .IsCurrent%}active{%end%} item" href="{%$.URI%}?{% $.Params.PageParams%}&page={%.Num%}">
{%.Num%}
</a>
{{{end}}}
{{{end}}}
{%end%}
{%end%}
{{{if .Pagination.HasNext}}}
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.Next}}}">
{%if .Pagination.HasNext%}
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.Next%}">
<i class="icon right arrow"></i>
</a>
{{{end}}}
{{{if not .Pagination.IsLast}}}
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.TotalPages}}}">
{%end%}
{%if not .Pagination.IsLast%}
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.TotalPages%}">
尾页
</a>
{{{end}}}
<div class="item">{{{.Pagination.Total}}}/{{{.Pagination.TotalPages}}}</div>
{%end%}
<div class="item">{%.Pagination.Total%}/{%.Pagination.TotalPages%}</div>
</div>
{{{end}}}
{%end%}
+2 -2
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<script>
swal({
title: "404 - NOT FOUND",
@@ -9,4 +9,4 @@
location.href = "/"
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<script>
swal({
title: "500 - INTERNAL SERVER ERROR",
@@ -8,4 +8,4 @@
confirmButtonText: "确定"
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,3 +1,3 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+9 -9
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "host/menu" . }}}
{% template "host/menu" . %}
<div class="twelve wide column">
<div class="pageHeader">
@@ -14,24 +14,24 @@
</div>
</div>
<form class="ui form fluid vertical segment">
<input type="hidden" name="id" value="{{{.Host.Id}}}">
<input type="hidden" name="id" value="{%.Host.Id%}">
<div class="four fields">
<div class="field">
<label>主机名</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.Host.Name}}}" placeholder="127.0.0.1">
<input type="text" name="name" value="{%.Host.Name%}" placeholder="127.0.0.1">
</div>
</div>
<div class="field">
<label>端口</label>
<div class="ui small input">
<input type="text" name="port" value="{{{.Host.Port}}}" placeholder="5921">
<input type="text" name="port" value="{%.Host.Port%}" placeholder="5921">
</div>
</div>
<div class="field">
<label>节点名称</label>
<div class="ui small input">
<input type="text" name="alias" value="{{{.Host.Alias}}}"
<input type="text" name="alias" value="{%.Host.Alias%}"
placeholder="节点名称如web">
</div>
</div>
@@ -40,13 +40,13 @@
<div class="field">
<label>备注</label>
<div class="ui small input">
<textarea rows="7" name="remark" >{{{.Host.Remark}}}</textarea>
<textarea rows="7" name="remark" >{%.Host.Remark%}</textarea>
</div>
</div>
</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>
@@ -113,4 +113,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+24 -18
View File
@@ -1,10 +1,11 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "host/menu" . }}}
{% 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">
@@ -16,13 +17,14 @@
</h3>
</div>
</div>
{%end%}
<form class="ui form">
<div class="three fields">
<div class="field">
<input type="text" placeholder="ID" name="id" value="{{{if gt .Params.Id 0}}}{{{.Params.Id}}}{{{end}}}">
<input type="text" placeholder="ID" name="id" value="{%if gt .Params.Id 0%}{%.Params.Id%}{%end%}">
</div>
<div class="field">
<input type="text" placeholder="主机名" name="name" value="{{{.Params.Name}}}">
<input type="text" placeholder="主机名" name="name" value="{%.Params.Name%}">
</div>
<div class="field">
<button class="ui linkedin submit button">搜索</button>
@@ -41,26 +43,30 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Hosts}}}
{%range $i, $v := .Hosts%}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{.Alias}}}</td>
<td>{{{.Port}}}</td>
<td>{{{.Remark}}}</td>
<td>{%.Id%}</td>
<td>{%.Name%}</td>
<td>{%.Alias%}</td>
<td>{%.Port%}</td>
<td>{%.Remark%}</td>
<td class="operation">
<a class="ui purple button" href="/host/edit/{{{.Id}}}">编辑</a>
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button><br>
{%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>
<button class="ui blue button" @click="ping({{{.Id}}})">连接测试</button>
<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>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -77,4 +83,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="four wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/host"}}}active teal{{{end}}} item" href="/host">
<a class="{%if eq .URI "/host"%}active teal{%end%} item" href="/host">
<i class="linux icon"></i> 节点列表
</a>
</div>
+3 -3
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "install/menu" . }}}
{% template "install/menu" . %}
<div class="ten wide column">
<form class="ui form">
<div class="ui blue center aligned segment">
@@ -186,4 +186,4 @@
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+9 -9
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "manage/menu" . }}}
{% template "manage/menu" . %}
<div class="twelve wide column">
<div class="pageHeader">
@@ -22,18 +22,18 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .LoginLogs}}}
{%range $i, $v := .LoginLogs%}
<tr>
<td>{{{.Username}}}</td>
<td>{{{.Ip}}}</td>
<td>{{{.Created}}}</td>
<td>{%.Username%}</td>
<td>{%.Ip%}</td>
<td>{%.Created%}</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+13 -13
View File
@@ -1,12 +1,12 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "manage/menu" .}}}
{%template "manage/menu" .%}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
{{{.Title}}}
{%.Title%}
</div>
</h3>
</div>
@@ -19,7 +19,7 @@
SMTP服务器
</label>
<div class="ui small input">
<input type="text" name="host" value="{{{.Mail.Host}}}">
<input type="text" name="host" value="{%.Mail.Host%}">
</div>
</div>
<div class="field">
@@ -27,7 +27,7 @@
端口
</label>
<div class="ui small input">
<input type="text" name="port" value="{{{if gt .Mail.Port 0}}}{{{.Mail.Port}}}{{{end}}}">
<input type="text" name="port" value="{%if gt .Mail.Port 0%}{%.Mail.Port%}{%end%}">
</div>
</div>
<div class="field">
@@ -35,7 +35,7 @@
用户名
</label>
<div class="ui small input">
<input type="text" name="user" value="{{{.Mail.User}}}">
<input type="text" name="user" value="{%.Mail.User%}">
</div>
</div>
<div class="field">
@@ -43,25 +43,25 @@
密码
</label>
<div class="ui small input">
<input type="text" name="password" value="{{{.Mail.Password}}}">
<input type="text" name="password" value="{%.Mail.Password%}">
</div>
</div>
</div>
<button class="ui primary button">保存</button>
{{{if .Mail.Host}}}
{%if .Mail.Host%}
<a class="ui green button" onclick="clearMailServer()">删除</a>
{{{end}}}
{%end%}
<br><br><br>
<div>
<div class="content">邮箱用户</div><p></p>
<div class="fields">
{{{range $i, $v := .Mail.MailUsers}}}
{%range $i, $v := .Mail.MailUsers%}
<div class="field">
<div class="ui segment">
{{{.Username}}}-{{{.Email}}}&nbsp;&nbsp;&nbsp;<div class="ui blue button" onclick="removeMailUser({{{.Id}}})">删除</div>
{%.Username%}-{%.Email%}&nbsp;&nbsp;&nbsp;<div class="ui blue button" onclick="removeMailUser({%.Id%})">删除</div>
</div>
</div>
{{{end}}}
{%end%}
</div>
</div>
</form>
@@ -198,4 +198,4 @@
});
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+3 -3
View File
@@ -1,13 +1,13 @@
<div class="three wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/manage/slack/edit"}}}active teal{{{end}}} item" href="/manage/slack/edit">
<a class="{%if eq .URI "/manage/slack/edit"%}active teal{%end%} item" href="/manage/slack/edit">
<i class="slack icon"></i> Slack配置
</a>
<a class="{{{if eq .URI "/manage/mail/edit"}}}active teal{{{end}}} item" href="/manage/mail/edit">
<a class="{%if eq .URI "/manage/mail/edit"%}active teal{%end%} item" href="/manage/mail/edit">
<i class="slack icon"></i> 邮件配置
</a>
<a class="{{{if eq .URI "/manage/login-log"}}}active teal{{{end}}} item" href="/manage/login-log">
<a class="{%if eq .URI "/manage/login-log"%}active teal{%end%} item" href="/manage/login-log">
<i class="slack icon"></i> 登录日志
</a>
</div>
+8 -8
View File
@@ -1,12 +1,12 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "manage/menu" .}}}
{%template "manage/menu" .%}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
{{{.Title}}}
{%.Title%}
</div>
</h3>
</div>
@@ -18,7 +18,7 @@
<div class="content">Slack WebHook URL</div>
</label>
<div class="ui small input">
<input type="text" id="url" value="{{{.Slack.Url}}}">
<input type="text" id="url" value="{%.Slack.Url%}">
</div>
</div>
<div class="ui primary button" @click="updateUrl">保存</div>
@@ -27,13 +27,13 @@
<div>
<div class="content">Slack Channel(配置任务通知时可选择多Channel)</div><p></p>
<div class="fields">
{{{range $i, $v := .Slack.Channels}}}
{%range $i, $v := .Slack.Channels%}
<div class="field">
<div class="ui segment">
{{{.Name}}}&nbsp;&nbsp;&nbsp;<div class="ui blue button" @click="removeChannel({{{.Id}}})">删除</div>
{%.Name%}&nbsp;&nbsp;&nbsp;<div class="ui blue button" @click="removeChannel({%.Id%})">删除</div>
</div>
</div>
{{{end}}}
{%end%}
</div>
</div>
<div class="ui facebook button" @click="createChannel">新增Channel</div>
@@ -85,4 +85,4 @@
}
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+76 -61
View File
@@ -1,8 +1,9 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "task/menu" .}}}
{%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,39 +15,40 @@
</h3>
</div>
</div>
{%end%}
<form class="ui form">
<div class="six fields search">
<div class="one wide field">
<input type="text" placeholder="任务ID" name="id" value="{{{if gt .Params.Id 0}}}{{{.Params.Id}}}{{{end}}}">
<input type="text" placeholder="任务ID" name="id" value="{%if gt .Params.Id 0%}{%.Params.Id%}{%end%}">
</div>
<div class="field">
<input type="text" placeholder="任务名称" name="name" value="{{{.Params.Name}}}">
<input type="text" placeholder="任务名称" name="name" value="{%.Params.Name%}">
</div>
<div class="field">
<input type="text" placeholder="标签名称" name="tag" value="{{{.Params.Tag}}}">
<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>
{{{range $i, $v := .Hosts}}}
<option value="{{{.Id}}}" {{{if eq $.Params.HostId .Id }}} selected {{{end}}} >{{{.Alias}}}-{{{.Name}}}</option>
{{{end}}}
{%range $i, $v := .Hosts%}
<option value="{%.Id%}" {%if eq $.Params.HostId .Id %} selected {%end%} >{%.Alias%}-{%.Name%}</option>
{%end%}
</select>
</div>
<div class="field">
<select name="protocol" id="protocol">
<option value="0">执行方式</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>
<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="2" {{{if eq .Params.Status 1}}}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>
<div class="field">
@@ -54,21 +56,25 @@
</div>
</div>
</form>
<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>
{%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%}
<br>
<table class="ui celled table task-list">
<thead>
<tr>
<th>
<input type="checkbox" onclick="checkAll(this)" style="width:25px;height: 25px;">
</th>
{%if .IsAdmin%}
<th>
<input type="checkbox" onclick="checkAll(this)" style="width:25px;height: 25px;">
</th>
{%end%}
<th>任务ID</th>
<th>任务名称</th>
<th>任务类型</th>
@@ -84,54 +90,58 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
{%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>
<input type="checkbox"
class="sub-check"
data-id="{{{.Id}}}"
style="width:25px;height: 25px;">
</td>
<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}}}
{%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}}}
{%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/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;
<a href="/task/log?task_id={{{.Id}}}"><i class="bar chart icon big" title="查看日志"></i></a>
<a href="/task/log?task_id={%.Id%}"><i class="bar chart icon big" title="查看日志"></i></a>
</div>
</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -201,7 +211,12 @@
}
function remove(id, stopReload) {
util.post('/task/remove/' + id, {}, function () {
var url = '/task/remove/' + id;
if (stopReload === undefined) {
util.removeConfirm(url);
return;
}
util.post(url, {}, function () {
if (stopReload === undefined) {
location.reload();
}
@@ -224,4 +239,4 @@
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+51 -34
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<style type="text/css">
pre {
white-space: pre-wrap;
@@ -10,9 +10,10 @@
</style>
<div class="ui grid">
<!--the vertical menu-->
{{{ template "task/menu" . }}}
{% template "task/menu" . %}
<div class="twelve wide column">
{%if .IsAdmin%}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
@@ -22,25 +23,26 @@
</h3>
</div>
</div>
{%end%}
<form class="ui form">
<div class="six fields search">
<div class="field">
<input type="text" placeholder="任务ID" name="task_id" value="{{{if gt .Params.TaskId 0}}}{{{.Params.TaskId}}}{{{end}}}">
<input type="text" placeholder="任务ID" name="task_id" value="{%if gt .Params.TaskId 0%}{%.Params.TaskId%}{%end%}">
</div>
<div class="field">
<select name="protocol" id="protocol">
<option value="0">执行方式</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>
<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="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="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="4" {%if eq .Params.Status 3%}selected{%end%}>取消</option>
</select>
</div>
<div class="field">
@@ -63,47 +65,54 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Logs}}}
{%range $i, $v := .Logs%}
<tr>
<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}}} SHELL {{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{unescape .Hostname}}}</td>
<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%} SHELL {%end%}</td>
<td>{%.RetryTimes%}</td>
<td>{%unescape .Hostname%}</td>
<td>
{{{if and (ne .Status 3) (ne .Status 4)}}}
{{{if gt .TotalTime 0}}}{{{.TotalTime}}}{{{else}}}1{{{end}}}<br>
开始时间: {{{.StartTime.Format "2006-01-02 15:04:05" }}}<br>
{{{if ne .Status 1}}}
结束时间: {{{.EndTime.Format "2006-01-02 15:04:05" }}}
{{{end}}}
{{{end}}}
{%if and (ne .Status 3) (ne .Status 4)%}
{%if gt .TotalTime 0%}{%.TotalTime%}{%else%}1{%end%}<br>
开始时间: {%.StartTime.Format "2006-01-02 15:04:05" %}<br>
{%if ne .Status 1%}
结束时间: {%.EndTime.Format "2006-01-02 15:04:05" %}
{%end%}
{%end%}
</td>
<td>
{{{if eq .Status 2}}}
{%if eq .Status 2%}
成功
{{{else if eq .Status 1}}}
{%else if eq .Status 1%}
<span style="color:green">执行中</span>
{{{else if eq .Status 0}}}
{%else if eq .Status 0%}
<span style="color:red">失败</span>
{{{else if eq .Status 3}}}
{%else if eq .Status 3%}
<span style="color:#4499EE">取消</span>
{{{end}}}
{%end%}
</td>
<td>
{{{if or (eq .Status 2) (eq .Status 0)}}}
{%if or (eq .Status 2) (eq .Status 0)%}
<button class="ui small primary button"
onclick="showResult('{{{.Name}}}', '{{{.Command}}}', '{{{.Result}}}')"
onclick="showResult('{%.Name%}', '{%.Command%}', '{%.Result%}')"
>查看结果
</button>
{{{end}}}
{%end%}
{%if $.IsAdmin%}
{%if and (eq .Status 1) (eq .Protocol 2) %}
<button class="ui small blue button" onclick="stopTask({%.Id%}, {%.TaskId%})">停止任务</button>
{%end%}
{%end%}
</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -146,6 +155,14 @@
}).modal('refresh').modal('show');
}
function stopTask(id, taskId) {
util.confirm("确定要停止任务吗", function () {
util.post("/task/log/stop/", {id: id, task_id:taskId}, function (code, message) {
swal('提示', message, 'info');
});
});
}
function clearLog() {
util.confirm("确定要删除所有日志吗?", function() {
util.post("/task/log/clear",{}, function() {
@@ -154,4 +171,4 @@
});
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,10 +1,10 @@
<div class="three wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/task"}}}active teal{{{end}}} item" href="/task">
<a class="{%if eq .URI "/task"%}active teal{%end%} item" href="/task">
<i class="tasks icon"></i> 定时任务列表
</a>
<a class="item {{{if eq .URI "/task/log"}}}active teal{{{end}}} " href="/task/log">
<a class="item {%if eq .URI "/task/log"%}active teal{%end%} " href="/task/log">
<i class="bar chart icon"></i> 定时任务日志
</a>
</div>
+67 -39
View File
@@ -1,25 +1,25 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "task/menu" .}}}
{%template "task/menu" .%}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
{{{.Title}}}
{%.Title%}
</div>
</h3>
</div>
</div>
<form class="ui form fluid vertical segment">
<input type="hidden" name="id" value="{{{.Task.Id}}}">
<input type="hidden" name="id" value="{%.Task.Id%}">
<div class="two fields">
<div class="field">
<label>
<div class="content">任务名称</div>
</label>
<div class="ui small input">
<input type="text" name="name" placeholder="任务名称" value="{{{.Task.Name}}}">
<input type="text" name="name" placeholder="任务名称" value="{%.Task.Name%}">
</div>
</div>
@@ -28,7 +28,7 @@
<div class="content">标签名称</div>
</label>
<div class="ui small input">
<input type="text" name="tag" placeholder="标签用于任务分类" value="{{{.Task.Tag}}}">
<input type="text" name="tag" placeholder="标签用于任务分类" value="{%.Task.Tag%}">
</div>
</div>
</div>
@@ -41,9 +41,9 @@
任务类型新增后不能变更
</div>
</label>
<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 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>
@@ -58,8 +58,8 @@
</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>
<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">
@@ -71,7 +71,7 @@
</div>
</label>
<div class="ui small input">
<input type="text" name="dependency_task_id" placeholder="可选" value="{{{.Task.DependencyTaskId}}}">
<input type="text" name="dependency_task_id" placeholder="可选" value="{%.Task.DependencyTaskId%}">
</div>
</div>
</div>
@@ -83,7 +83,7 @@
</div>
</label>
<div class="ui small input">
<input type="text" name="spec" value="{{{.Task.Spec}}}" placeholder="秒 分 时 天 月 周"/>
<input type="text" name="spec" value="{%.Task.Spec%}" placeholder="秒 分 时 天 月 周"/>
</div>
</div>
</div>
@@ -92,9 +92,9 @@
<div class="field">
<label>执行方式</label>
<select name="protocol" id="protocol">
<option value="2" {{{if .Task}}} {{{if eq .Task.Protocol 2}}}selected{{{end}}} {{{end}}}
<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>
<option value="1" {%if .Task%} {%if eq .Task.Protocol 1%}selected{%end%} {%end%}>HTTP</option>
</select>
</div>
</div>
@@ -102,47 +102,64 @@
<div class="field">
<label>选择任务节点</label>
<div id="hostId">
{{{range $i, $v := .Hosts}}}
{%range $i, $v := .Hosts%}
<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}}}
<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}}}
{%end%}
</div> &nbsp; <br> <a class="ui blue button" href="/host/create" target="_blank">添加节点</a>
</div>
</div>
<div class="two fields" id="http-method" style="display: none">
<div class="field">
<label>请求方法</label>
<select name="http_method">
<option value="1" {%if .Task%} {%if eq .Task.HttpMethod 1%}selected{%end%} {%end%}>GET</option>
<option value="2" {%if .Task%} {%if eq .Task.HttpMethod 2%}selected{%end%} {%end%}
data-validate-type="selectProtocol">POST</option>
</select>
</div>
</div>
<div class="two fields">
<div class="field">
<label>命令</label>
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{{{.Task.Command}}}</textarea>
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{%.Task.Command%}</textarea>
</div>
</div>
<div class="three fields">
<div class="two 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}}}">
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{%if .Task%} {%.Task.Timeout%} {%else%}0{%end%}">
</div>
<div class="field">
<label>允许多实例同时运行</label>
<select name="multi">
<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>
<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="two fields">
<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>任务失败重试间隔时间 (, 0-3600)</label>
<input type="text" name="retry_interval" placeholder="默认0, 执行默认重试策略" value="{%if .Task%} {%.Task.RetryInterval%} {%else%}0{%end%}">
</div>
</div>
<div class="three fields">
<div class="field">
<label>任务通知</label>
<select name="notify_status" id="task-status">
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyStatus 0}}}selected{{{end}}} {{{end}}}>不通知</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyStatus 1}}}selected{{{end}}} {{{end}}}>失败通知</option>
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyStatus 2}}}selected{{{end}}} {{{end}}}>执行结束通知</option>
<option value="1"{%if .Task%} {%if eq .Task.NotifyStatus 0%}selected{%end%} {%end%}>不通知</option>
<option value="2" {%if .Task%} {%if eq .Task.NotifyStatus 1%}selected{%end%} {%end%}>失败通知</option>
<option value="3" {%if .Task%} {%if eq .Task.NotifyStatus 2%}selected{%end%} {%end%}>执行结束通知</option>
</select>
</div>
</div>
@@ -150,9 +167,9 @@
<div class="field" >
<label>通知类型</label>
<select name="notify_type">
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyType 0}}}selected{{{end}}} {{{end}}}>请选择</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyType 1}}}selected{{{end}}} {{{end}}}>邮件</option>
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyType 2}}}selected{{{end}}} {{{end}}}>Slack</option>
<option value="1"{%if .Task%} {%if eq .Task.NotifyType 0%}selected{%end%} {%end%}>请选择</option>
<option value="2" {%if .Task%} {%if eq .Task.NotifyType 1%}selected{%end%} {%end%}>邮件</option>
<option value="3" {%if .Task%} {%if eq .Task.NotifyType 2%}selected{%end%} {%end%}>Slack</option>
</select>
</div>
</div>
@@ -160,7 +177,7 @@
<div class="two fields">
<div class="field">
<label>备注</label>
<textarea rows="5" name="remark" placeholder="任务备注信息">{{{.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>
@@ -239,11 +256,11 @@
}
function showNotify() {
var notifyStatus = {{{.Task.NotifyStatus}}};
var notifyStatus = {%.Task.NotifyStatus%};
if (notifyStatus > 0) {
$('#task-notify-type').show();
}
var notifyReceiverIds = '{{{.Task.NotifyReceiverId}}}'.split(',');
var notifyReceiverIds = '{%.Task.NotifyReceiverId%}'.split(',');
changeNotify(notifyReceiverIds);
}
@@ -292,10 +309,12 @@
var protocol = $('#protocol').val();
if (protocol == 2) {
$('#hostField').show();
$('#http-method').hide();
return;
}
$('#hostField').hide();
$('#http-method').show();
}
$('.ui.checkbox')
@@ -423,6 +442,15 @@
}
]
},
retryInterval: {
identifier : 'retry_interval',
rules: [
{
type : 'integer[0..3600]',
prompt : '重试间隔时间0-3600'
}
]
},
remark: {
identifier : 'remark',
rules: [
@@ -437,4 +465,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+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" . %}
+5 -20
View File
@@ -1,14 +1,8 @@
{{{ template "common/header" . }}}
{% 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>
@@ -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: [
@@ -70,4 +55,4 @@
inline : true
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+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" . %}
+3 -3
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui small modal">
<div class="header">用户登录</div>
<div class="content">
@@ -15,7 +15,7 @@
</div>
<div class="two fields">
<div class="field">
{{{.Captcha.CreateHtml}}}
{%.Captcha.CreateHtml%}
<input type="text" name="captcha" placeholder="验证码">
</div>
</div>
@@ -71,4 +71,4 @@
inline : true
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+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" . %}
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# set -x -u
# 上传二进制包到七牛
if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then
echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need'
exit 1
fi
# 打包
for i in linux darwin windows
do
./build.sh -p $i
if [[ $? != 0 ]];then
break
fi
./build_node.sh -p $i
if [[ $? != 0 ]];then
break
fi
done
# 身份认证
qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY
# 上传
for i in `ls gocron*.gz gocron*.zip`
do
# 上传文件 qrsctl put bucket key srcFile
KEY=gocron/$i
qrsctl put github $KEY $i
if [[ $? != 0 ]];then
break
fi
echo "刷新七牛CDN-" $QINIU_URL/$KEY
qrsctl cdn/refresh $QINIU_URL/$KEY
rm $i
done
echo '打包并上传成功'
+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 {
+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)
+1 -1
View File
@@ -90,7 +90,7 @@ type FuncJob func()
func (f FuncJob) Run() { f() }
// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func(), name string) error {
func (c *Cron) AddFunc(spec string, cmd func(), name string) error {
return c.AddJob(spec, FuncJob(cmd), name)
}
+6 -6
View File
@@ -1,12 +1,12 @@
package cron
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
"fmt"
)
// Parse returns a new crontab schedule representing the given spec.
@@ -160,7 +160,7 @@ func all(r bounds) uint64 {
// parseDescriptor returns a pre-defined schedule for the expression, or panics
// if none matches.
func parseDescriptor(spec string) (Schedule,error) {
func parseDescriptor(spec string) (Schedule, error) {
switch spec {
case "@yearly", "@annually":
return &SpecSchedule{
@@ -217,10 +217,10 @@ func parseDescriptor(spec string) (Schedule,error) {
if strings.HasPrefix(spec, every) {
duration, err := time.ParseDuration(spec[len(every):])
if err != nil {
return nil, fmt.Errorf("Failed to parse duration %s: %s", spec, err)
}
return Every(duration),nil
return nil, fmt.Errorf("Failed to parse duration %s: %s", spec, err)
}
return Every(duration), nil
}
return nil, fmt.Errorf("Unrecognized descriptor: %s", spec)
return nil, fmt.Errorf("Unrecognized descriptor: %s", spec)
}
+4 -4
View File
@@ -383,8 +383,8 @@ func init() {
// 4253 and Oakley Group 2 in RFC 2409.
p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16)
kexAlgoMap[kexAlgoDH1SHA1] = &dhGroup{
g: new(big.Int).SetInt64(2),
p: p,
g: new(big.Int).SetInt64(2),
p: p,
pMinus1: new(big.Int).Sub(p, bigOne),
}
@@ -393,8 +393,8 @@ func init() {
p, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", 16)
kexAlgoMap[kexAlgoDH14SHA1] = &dhGroup{
g: new(big.Int).SetInt64(2),
p: p,
g: new(big.Int).SetInt64(2),
p: p,
pMinus1: new(big.Int).Sub(p, bigOne),
}