mirror of
https://github.com/ouqiang/gocron.git
synced 2024-04-21 12:31:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
960fc988b1 | ||
|
|
7a949a22bf | ||
|
|
c59f8b730a | ||
|
|
ac2f01f144 | ||
|
|
09834ffaf6 | ||
|
|
93c544f0d4 | ||
|
|
9f9c663624 | ||
|
|
beeda4b54b | ||
|
|
fc7daa83ef | ||
|
|
9215153f6f | ||
|
|
e335899fbb | ||
|
|
bdd19955a6 | ||
|
|
b0012f3ee7 | ||
|
|
ec4a4c0855 | ||
|
|
bf76393073 | ||
|
|
372a5e2d8a | ||
|
|
c997ce7cab | ||
|
|
09985998ca | ||
|
|
aab450a3bf | ||
|
|
0b0502d34c | ||
|
|
b3632006bb | ||
|
|
25168cab2a | ||
|
|
ea18109b92 | ||
|
|
370645fca8 | ||
|
|
c6b3cf1509 | ||
|
|
36274566b0 | ||
|
|
45662b1c3f | ||
|
|
c40ef9aaad | ||
|
|
43c267784d | ||
|
|
1ce41448c2 | ||
|
|
03afad0864 | ||
|
|
233124bbfb | ||
|
|
88e0a296cb | ||
|
|
868b4ed12a | ||
|
|
8f547b7e8d | ||
|
|
17aac50cbf | ||
|
|
50f08e996a | ||
|
|
0e023bd3fa | ||
|
|
4f7a5cfbde | ||
|
|
f24d306026 | ||
|
|
7b1086bdbc | ||
|
|
1c6dd1ca6a | ||
|
|
e497261789 | ||
|
|
06859f3464 | ||
|
|
6606f64a4b | ||
|
|
ac8936e238 | ||
|
|
adbc31f8d4 | ||
|
|
16b9a7702b | ||
|
|
2bae3cf9ed | ||
|
|
64128f37e0 | ||
|
|
83f8b67b20 | ||
|
|
b10cff294c | ||
|
|
32caa5a4b7 | ||
|
|
738b168689 | ||
|
|
e1fa3cf645 | ||
|
|
3ed64f9f22 | ||
|
|
94c9963e81 | ||
|
|
4c940c2088 | ||
|
|
50638e6dfa | ||
|
|
f081d89fb4 | ||
|
|
fd39434446 | ||
|
|
72349248ae | ||
|
|
02e525ab83 | ||
|
|
0950fc69f9 | ||
|
|
750fb49a37 | ||
|
|
68d569a6de | ||
|
|
3fd5b2ee55 | ||
|
|
e9b475a230 | ||
|
|
8720c18654 | ||
|
|
4daf0a15ba | ||
|
|
3474636e23 | ||
|
|
f0ff9a88a7 | ||
|
|
21028ec6f0 | ||
|
|
c4af1653ff | ||
|
|
250cbdde7c | ||
|
|
a966f3aeda | ||
|
|
3c02d55ebf | ||
|
|
4009e845cb | ||
|
|
37f2405ab3 | ||
|
|
8ec80a02b6 | ||
|
|
601f250882 | ||
|
|
019fee2cce | ||
|
|
d4e0898674 | ||
|
|
d642c9641d | ||
|
|
2ba8cb67c8 | ||
|
|
350dc0881e | ||
|
|
db1ef3b317 | ||
|
|
1c2696798c | ||
|
|
2b1c7f16cf | ||
|
|
337ee35357 | ||
|
|
b509fcec55 | ||
|
|
6e8622e4f3 | ||
|
|
ff2228ed50 | ||
|
|
5d97ffad3b | ||
|
|
d13f1997dd | ||
|
|
5b8a8ae890 | ||
|
|
3d61c7694b | ||
|
|
3e8c3a236e | ||
|
|
e6560e8483 | ||
|
|
7e010d0aca | ||
|
|
b8f13b4b0e | ||
|
|
eb02804aec | ||
|
|
eb7d8d7bf5 | ||
|
|
b5d476b339 | ||
|
|
a78d14693b | ||
|
|
157e899e06 | ||
|
|
90a3136ce5 | ||
|
|
22069aa156 | ||
|
|
3de70760a0 |
@@ -0,0 +1,2 @@
|
||||
.git
|
||||
web/vue/node_modules
|
||||
+13
-10
@@ -23,15 +23,18 @@ _testmain.go
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
log/*
|
||||
data/*
|
||||
conf/install.lock
|
||||
conf/app.ini
|
||||
conf/ansible_hosts.ini
|
||||
log
|
||||
data
|
||||
conf
|
||||
profile/*
|
||||
public/resource/javascript/vue.js
|
||||
gocron
|
||||
gocron.exe
|
||||
gocron-node
|
||||
gocron-node.exe
|
||||
/gocron
|
||||
/gocron-node
|
||||
/bin
|
||||
/web/public/static
|
||||
/web/public/index.html
|
||||
/gocron-package
|
||||
/gocron-node-package
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.7.x
|
||||
script: go test `go list ./... | grep -v vendor`
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
FROM golang:1.15-alpine as builder
|
||||
|
||||
RUN apk update \
|
||||
&& apk add --no-cache git ca-certificates make bash yarn nodejs
|
||||
|
||||
RUN go env -w GO111MODULE=on && \
|
||||
go env -w GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN git clone https://github.com/ouqiang/gocron.git \
|
||||
&& cd gocron \
|
||||
&& yarn config set ignore-engines true \
|
||||
&& make install-vue \
|
||||
&& make build-vue \
|
||||
&& make statik \
|
||||
&& CGO_ENABLED=0 make gocron
|
||||
|
||||
FROM alpine:3.12
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S app \
|
||||
&& adduser -S -g app app
|
||||
|
||||
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/gocron/bin/gocron .
|
||||
|
||||
RUN chown -R app:app ./
|
||||
|
||||
EXPOSE 5920
|
||||
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["/app/gocron", "web"]
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM alpine:3.7
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S app \
|
||||
&& adduser -S -g app app
|
||||
|
||||
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY gocron .
|
||||
|
||||
RUN chown -R app:app ./
|
||||
|
||||
EXPOSE 5920
|
||||
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["/app/gocron", "web"]
|
||||
@@ -1,26 +1,32 @@
|
||||
[](https://travis-ci.org/ouqiang/gocron)
|
||||
# gocron - 定时任务管理系统
|
||||
[](https://github.com/ouqiang/gocron/releases)
|
||||
[](https://github.com/ouqiang/gocron/blob/master/LICENSE)
|
||||
[](https://github.com/ouqiang/gocron/releases)
|
||||
|
||||
# 项目简介
|
||||
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
|
||||
使用Go语言开发的轻量级定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
|
||||
|
||||
原有的延时任务拆分为独立项目[延迟队列](https://github.com/ouqiang/delay-queue)
|
||||
|
||||
## 功能特性
|
||||
* Web界面管理定时任务, 支持动态添加、删除、编辑任务
|
||||
* crontab时间表达式,精确到秒
|
||||
* 任务执行失败重试设置
|
||||
* 任务超时设置
|
||||
* 延时任务
|
||||
* Web界面管理定时任务
|
||||
* crontab时间表达式, 精确到秒
|
||||
* 任务执行失败可重试
|
||||
* 任务执行超时, 强制结束
|
||||
* 任务依赖配置, A任务完成后再执行B任务
|
||||
* 账户权限控制
|
||||
* 任务类型
|
||||
* shell任务
|
||||
> 在远程服务器上执行shell命令, 调度器与任务执行器保持长连接
|
||||
> 在任务节点上执行shell命令, 支持任务同时在多个节点上运行
|
||||
* HTTP任务
|
||||
> 访问指定的URL地址
|
||||
* 查看任务执行日志
|
||||
* 任务执行结果通知, 支持邮件、Slack
|
||||
> 访问指定的URL地址, 由调度器直接执行, 不依赖任务节点
|
||||
* 查看任务执行结果日志
|
||||
* 任务执行结果通知, 支持邮件、Slack、Webhook
|
||||
|
||||
### 截图
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 支持平台
|
||||
> Windows、Linux、Mac OS
|
||||
@@ -30,55 +36,142 @@
|
||||
|
||||
|
||||
## 下载
|
||||
* 调度器(管理后台)
|
||||
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-linux-amd64.tar.gz)
|
||||
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-darwin-amd64.tar.gz)
|
||||
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-windows-amd64.zip)
|
||||
* 任务执行器(安装在远程主机上, 执行shell命令需安装)
|
||||
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-linux-amd64.tar.gz)
|
||||
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-darwin-amd64.tar.gz)
|
||||
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-windows-amd64.zip)
|
||||
[releases](https://github.com/ouqiang/gocron/releases)
|
||||
|
||||
[版本升级](https://github.com/ouqiang/gocron/wiki/版本升级)
|
||||
|
||||
## 安装
|
||||
|
||||
### 二进制安装
|
||||
1. 解压压缩包
|
||||
1. 解压压缩包
|
||||
2. `cd 解压目录`
|
||||
3. 启动
|
||||
* 调度器启动
|
||||
* Windows: `gocron.exe web`
|
||||
* Linux、Mac OS: `./gocron web`
|
||||
* 任务执行器启动
|
||||
* Windows: `gocron-node.exe ip:port (默认0.0.0.0:5921)`
|
||||
* Linux、Mac OS: `./gocron-node ip:port (默认0.0.0.0:5921)`
|
||||
3. 启动
|
||||
* 调度器启动
|
||||
* Windows: `gocron.exe web`
|
||||
* Linux、Mac OS: `./gocron web`
|
||||
* 任务节点启动, 默认监听0.0.0.0:5921
|
||||
* Windows: `gocron-node.exe`
|
||||
* Linux、Mac OS: `./gocron-node`
|
||||
4. 浏览器访问 http://localhost:5920
|
||||
|
||||
### 源码安装
|
||||
1. `go`语言版本1.7+
|
||||
2. `go get -d github.com/ouqiang/gocron`
|
||||
3. 编译
|
||||
* 调度器 `go build -tags gocron -o gocron`
|
||||
* 任务执行器 `go build -tags node -o gocron-node`
|
||||
4. 启动、访问方式同上
|
||||
|
||||
- 安装Go 1.11+
|
||||
- `go get -d github.com/ouqiang/gocron`
|
||||
- `export GO111MODULE=on`
|
||||
- 编译 `make`
|
||||
- 启动
|
||||
* gocron `./bin/gocron web`
|
||||
* gocron-node `./bin/gocron-node`
|
||||
|
||||
|
||||
### docker
|
||||
|
||||
```shell
|
||||
docker run --name gocron --link mysql:db -p 5920:5920 -d ouqg/gocron
|
||||
```
|
||||
|
||||
配置: /app/conf/app.ini
|
||||
|
||||
日志: /app/log/cron.log
|
||||
|
||||
镜像不包含gocron-node, gocron-node需要和具体业务一起构建
|
||||
|
||||
|
||||
### 开发
|
||||
|
||||
1. 安装Go1.9+, Node.js, Yarn
|
||||
2. 安装前端依赖 `make install-vue`
|
||||
3. 启动gocron, gocron-node `make run`
|
||||
4. 启动node server `make run-vue`, 访问地址 http://localhost:8080
|
||||
|
||||
访问http://localhost:8080, API请求会转发给gocron
|
||||
|
||||
`make` 编译
|
||||
|
||||
`make run` 编译并运行
|
||||
|
||||
`make package` 打包
|
||||
> 生成当前系统的压缩包 gocron-v1.5-darwin-amd64.tar.gz gocron-node-v1.5-darwin-amd64.tar.gz
|
||||
|
||||
`make package-all` 生成Windows、Linux、Mac的压缩包
|
||||
|
||||
### 命令
|
||||
|
||||
* gocron
|
||||
* -v 查看版本
|
||||
|
||||
* gocron web
|
||||
* --host 默认0.0.0.0
|
||||
* -p 端口, 指定端口, 默认5920
|
||||
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
|
||||
* -d 后台运行
|
||||
* -h 查看帮助
|
||||
* gocron serv
|
||||
* -s stop|status stop:停止gocron status:查看运行状态
|
||||
* gocron-node ip:port, 默认0.0.0.0:5921
|
||||
* gocron-node
|
||||
* -allow-root *nix平台允许以root用户运行
|
||||
* -s ip:port 监听地址
|
||||
* -enable-tls 开启TLS
|
||||
* -ca-file CA证书文件
|
||||
* -cert-file 证书文件
|
||||
* -key-file 私钥文件
|
||||
* -h 查看帮助
|
||||
* -v 查看版本
|
||||
|
||||
## To Do List
|
||||
- [x] 版本升级
|
||||
- [x] 批量开启、关闭、删除任务
|
||||
- [x] 调度器与任务节点通信支持https
|
||||
- [x] 任务分组
|
||||
- [x] 多用户
|
||||
- [x] 权限控制
|
||||
|
||||
## 程序使用的组件
|
||||
* web框架 [Macaron](http://go-macaron.com/)
|
||||
* 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)
|
||||
* UI框架 [Element UI](https://github.com/ElemeFE/element)
|
||||
* 依赖管理 [Govendor](https://github.com/kardianos/govendor)
|
||||
* RPC框架 [gRPC](https://github.com/grpc/grpc)
|
||||
|
||||
## 反馈
|
||||
提交[issue](https://github.com/ouqiang/gocron/issues/new)
|
||||
|
||||
## ChangeLog
|
||||
|
||||
v1.5
|
||||
--------
|
||||
* 前端使用Vue+ElementUI重构
|
||||
* 任务通知
|
||||
* 新增WebHook通知
|
||||
* 自定义通知模板
|
||||
* 匹配任务执行结果关键字发送通知
|
||||
* 任务列表页显示任务下次执行时间
|
||||
|
||||
v1.4
|
||||
--------
|
||||
* HTTP任务支持POST请求
|
||||
* 后台手动停止运行中的shell任务
|
||||
* 任务执行失败重试间隔时间支持用户自定义
|
||||
* 修复API接口调用报403错误
|
||||
|
||||
v1.3
|
||||
--------
|
||||
* 支持多用户登录
|
||||
* 增加用户权限控制
|
||||
|
||||
|
||||
v1.2.2
|
||||
--------
|
||||
* 用户登录页增加图形验证码
|
||||
* 支持从旧版本升级
|
||||
* 任务批量开启、关闭、删除
|
||||
* 调度器与任务节点支持HTTPS双向认证
|
||||
* 修复任务列表页总记录数显示错误
|
||||
|
||||
v1.1
|
||||
--------
|
||||
|
||||
* 任务可同时在多个节点上运行
|
||||
* *nix平台默认禁止以root用户运行任务节点
|
||||
* 子任务命令中增加预定义占位符, 子任务可根据主任务运行结果执行相应操作
|
||||
* 删除守护进程模块
|
||||
* Web访问日志输出到终端
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
@@ -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 -H windowsgui'
|
||||
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
|
||||
@@ -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 -H windowsgui' -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
|
||||
@@ -0,0 +1,199 @@
|
||||
// Command gocron
|
||||
//go:generate statik -src=../../web/public -dest=../../internal -f
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/setting"
|
||||
"github.com/ouqiang/gocron/internal/routers"
|
||||
"github.com/ouqiang/gocron/internal/service"
|
||||
"github.com/ouqiang/goutil"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
AppVersion = "1.5"
|
||||
BuildDate, GitCommit string
|
||||
)
|
||||
|
||||
// web服务器默认端口
|
||||
const DefaultPort = 5920
|
||||
|
||||
func main() {
|
||||
cliApp := cli.NewApp()
|
||||
cliApp.Name = "gocron"
|
||||
cliApp.Usage = "gocron service"
|
||||
cliApp.Version, _ = goutil.FormatAppVersion(AppVersion, GitCommit, BuildDate)
|
||||
cliApp.Commands = getCommands()
|
||||
cliApp.Flags = append(cliApp.Flags, []cli.Flag{}...)
|
||||
err := cliApp.Run(os.Args)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(AppVersion)
|
||||
// 初始化模块 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, 1)
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Command gocron-node
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/auth"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/server"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/goutil"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
AppVersion, BuildDate, GitCommit string
|
||||
)
|
||||
|
||||
func main() {
|
||||
var serverAddr string
|
||||
var allowRoot bool
|
||||
var version bool
|
||||
var CAFile string
|
||||
var certFile string
|
||||
var keyFile string
|
||||
var enableTLS bool
|
||||
var logLevel string
|
||||
flag.BoolVar(&allowRoot, "allow-root", false, "./gocron-node -allow-root")
|
||||
flag.StringVar(&serverAddr, "s", "0.0.0.0:5921", "./gocron-node -s ip:port")
|
||||
flag.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.StringVar(&logLevel, "log-level", "info", "-log-level error")
|
||||
flag.Parse()
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.SetLevel(level)
|
||||
|
||||
if version {
|
||||
goutil.PrintAppVersion(AppVersion, GitCommit, BuildDate)
|
||||
return
|
||||
}
|
||||
|
||||
if enableTLS {
|
||||
if !utils.FileExist(CAFile) {
|
||||
log.Fatalf("failed to read ca cert file: %s", CAFile)
|
||||
}
|
||||
if !utils.FileExist(certFile) {
|
||||
log.Fatalf("failed to read server cert file: %s", certFile)
|
||||
return
|
||||
}
|
||||
if !utils.FileExist(keyFile) {
|
||||
log.Fatalf("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 {
|
||||
log.Fatal("Do not run gocron-node as root user")
|
||||
return
|
||||
}
|
||||
|
||||
server.Start(serverAddr, enableTLS, certificate)
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var CmdServ = cli.Command{
|
||||
Name: "serv",
|
||||
Usage: "manage gocron, ./gocron serv -s stop|status",
|
||||
Action: runServ,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "s",
|
||||
Value:"",
|
||||
Usage: "stop|status",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runServ(ctx *cli.Context) {
|
||||
if utils.IsWindows() {
|
||||
fmt.Println("not support on windows")
|
||||
return
|
||||
}
|
||||
option := ctx.String("s")
|
||||
if !utils.InStringSlice([]string{"stop", "status"}, option) {
|
||||
fmt.Println("invalid option")
|
||||
return
|
||||
}
|
||||
app.InitEnv()
|
||||
pid := app.GetPid()
|
||||
if pid <= 0 {
|
||||
fmt.Println("not running")
|
||||
return
|
||||
}
|
||||
process ,err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
fmt.Println("not running", err)
|
||||
return
|
||||
}
|
||||
switch option {
|
||||
case "stop":
|
||||
stop(process)
|
||||
case "status":
|
||||
status(process)
|
||||
}
|
||||
}
|
||||
|
||||
func stop(process *os.Process) {
|
||||
fmt.Println("stopping gocron......")
|
||||
err := process.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
fmt.Println("failed to kill process", err)
|
||||
} else {
|
||||
fmt.Println("stopped")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func status(process *os.Process) {
|
||||
fmt.Printf("running, pid-[%d]\n", process.Pid)
|
||||
}
|
||||
-239
@@ -1,239 +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"
|
||||
"io"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"os/exec"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
||||
)
|
||||
|
||||
// web服务器默认端口
|
||||
const DefaultPort = 5920
|
||||
|
||||
const InitProcess = 1
|
||||
|
||||
var CmdWeb = cli.Command{
|
||||
Name: "web",
|
||||
Usage: "run web server",
|
||||
Action: runWeb,
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "port,p",
|
||||
Value: DefaultPort,
|
||||
Usage: "bind port",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "env,e",
|
||||
Value: "prod",
|
||||
Usage: "runtime environment, dev|test|prod",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "d",
|
||||
Usage: "-d=true, run as daemon process",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runWeb(ctx *cli.Context) {
|
||||
// 设置守护进程
|
||||
becomeDaemon(ctx)
|
||||
// 设置运行环境
|
||||
setEnvironment(ctx)
|
||||
// 初始化应用
|
||||
app.InitEnv()
|
||||
app.WritePid()
|
||||
// 初始化模块 DB、定时任务等
|
||||
initModule()
|
||||
// 捕捉信号,配置热更新等
|
||||
go catchSignal()
|
||||
m := macaron.NewWithLogger(getWebLogWriter())
|
||||
|
||||
// 注册路由
|
||||
routers.Register(m)
|
||||
// 注册中间件.
|
||||
routers.RegisterMiddleware(m)
|
||||
port := parsePort(ctx)
|
||||
fmt.Println("server start")
|
||||
m.Run(port)
|
||||
}
|
||||
|
||||
func becomeDaemon(ctx *cli.Context) {
|
||||
// 不支持windows
|
||||
if utils.IsWindows() {
|
||||
return
|
||||
}
|
||||
if !ctx.IsSet("d") {
|
||||
return
|
||||
}
|
||||
|
||||
if os.Getppid() == InitProcess {
|
||||
// 子进程不再处理
|
||||
return
|
||||
}
|
||||
|
||||
filePath, _:= filepath.Abs(os.Args[0])
|
||||
cmd := exec.Command(filePath, os.Args[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
logger.Fatal("创建守护进程失败", err)
|
||||
}
|
||||
|
||||
// 父进程退出, 子进程由init-1号进程收养
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func initModule() {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := setting.Read(app.AppConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("读取应用配置失败", err)
|
||||
}
|
||||
app.Setting = config
|
||||
|
||||
models.Db = models.CreateDb()
|
||||
|
||||
// 初始化定时任务
|
||||
serviceTask := new(service.Task)
|
||||
serviceTask.Initialize()
|
||||
|
||||
// 初始化延时任务
|
||||
delayTaskEnabled, err := config.Key("delay.task.enable").Bool()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !delayTaskEnabled {
|
||||
return
|
||||
}
|
||||
delayTaskSlots, err := config.Key("delay.task.slots").Int()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delayTaskTick := config.Key("delay.task.tick").String()
|
||||
tick, err := time.ParseDuration(delayTaskTick)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
serviceDelayTask := new(service.DelayTask)
|
||||
serviceDelayTask.Initialize(tick, delayTaskSlots)
|
||||
}
|
||||
|
||||
// 解析端口
|
||||
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 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 getWebLogWriter() io.Writer {
|
||||
if macaron.Env == macaron.DEV {
|
||||
return os.Stdout
|
||||
}
|
||||
logFile := app.LogDir + "/access.log"
|
||||
var err error
|
||||
w, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND ,0666)
|
||||
if err != nil {
|
||||
fmt.Printf("日志文件[%s]打开失败", logFile)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// 应用退出
|
||||
func shutdown() {
|
||||
defer func() {
|
||||
app.RemovePid()
|
||||
logger.Info("已退出")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
logger.Info("应用准备退出")
|
||||
serviceTask := new(service.Task)
|
||||
// 停止所有任务调度
|
||||
logger.Info("停止定时任务调度")
|
||||
serviceTask.StopAll()
|
||||
delayTaskEnable, _ := app.Setting.Key("delay.task.enable").Bool()
|
||||
if delayTaskEnable {
|
||||
logger.Info("停止延时任务调度")
|
||||
serviceDelayTask := new(service.DelayTask)
|
||||
serviceDelayTask.Stop()
|
||||
}
|
||||
// 释放gRPC连接池
|
||||
grpcpool.Pool.ReleaseAll()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
module github.com/ouqiang/gocron
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/Tang-RoseChild/mahonia v0.0.0-20131226213531-0eef680515cc
|
||||
github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755 // indirect
|
||||
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||
github.com/go-macaron/binding v0.0.0-20170611065819-ac54ee249c27
|
||||
github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 // indirect
|
||||
github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/go-xorm/builder v0.3.4 // indirect
|
||||
github.com/go-xorm/core v0.6.2
|
||||
github.com/go-xorm/xorm v0.7.1
|
||||
github.com/golang/protobuf v1.3.1
|
||||
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5
|
||||
github.com/klauspost/compress v1.5.0 // indirect
|
||||
github.com/klauspost/cpuid v1.2.1 // indirect
|
||||
github.com/lib/pq v1.1.1
|
||||
github.com/ouqiang/goutil v1.1.1
|
||||
github.com/rakyll/statik v0.1.6
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/urfave/cli v1.20.0
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 // indirect
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 // indirect
|
||||
google.golang.org/grpc v1.21.0
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
||||
gopkg.in/ini.v1 v1.42.0
|
||||
gopkg.in/macaron.v1 v1.3.2
|
||||
)
|
||||
@@ -0,0 +1,155 @@
|
||||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Tang-RoseChild/mahonia v0.0.0-20131226213531-0eef680515cc h1:7QGNag2vwx98HBiQ8A9M2PWO0ws/xNU6eZVlAbvIWZw=
|
||||
github.com/Tang-RoseChild/mahonia v0.0.0-20131226213531-0eef680515cc/go.mod h1:Dl60+a8Tcs+dl3UbHuY54vuwwXo6JJ22RwnDGZ+0U+o=
|
||||
github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755 h1:1B7wb36fHLSwZfHg6ngZhhtIEHQjiC5H4p7qQGBEffg=
|
||||
github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755/go.mod h1:voKvFVpXBJxdIPeqjoJuLK+UVcRlo/JLjeToGxPYu68=
|
||||
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs=
|
||||
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
|
||||
github.com/go-macaron/binding v0.0.0-20170611065819-ac54ee249c27 h1:ieTTL9+RAtpcMaskwWNN19Vdrsl4X+k2CWsQijWNsAU=
|
||||
github.com/go-macaron/binding v0.0.0-20170611065819-ac54ee249c27/go.mod h1:u+H6rwW+HQwUL+w5uaEJSpIlVZDye1o9MB4Su0JfRfM=
|
||||
github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07 h1:YSIA98PevNf1NtCa/J6cz7gjzpz99WVAOa9Eg0klKps=
|
||||
github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07/go.mod h1://cJFfDp/70L0oTNAMB+M8Jd0rpuIx/55iARuJ6StwE=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
|
||||
github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 h1:3wYKrRg9IjUMfaf3H0Hh7M5Li9ge79Y7aw2yujHa2jQ=
|
||||
github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90/go.mod h1:Ut/NmkIMGVYlEdJBzEZgWVWG5ZpYS9BLmUgXfAgi+qM=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-xorm/builder v0.3.2/go.mod h1:v8mE3MFBgtL+RGFNfUnAMUqqfk/Y4W5KuwCFQIEpQLk=
|
||||
github.com/go-xorm/builder v0.3.4 h1:FxkeGB4Cggdw3tPwutLCpfjng2jugfkg6LDMrd/KsoY=
|
||||
github.com/go-xorm/builder v0.3.4/go.mod h1:KxkQkNN1DpPKTedxXyTQcmH+rXfvk4LZ9SOOBoZBAxw=
|
||||
github.com/go-xorm/core v0.6.0/go.mod h1:d8FJ9Br8OGyQl12MCclmYBuBqqxsyeedpXciV5Myih8=
|
||||
github.com/go-xorm/core v0.6.2 h1:EJLcSxf336POJr670wKB55Mah9f93xzvGYzNRgnT8/Y=
|
||||
github.com/go-xorm/core v0.6.2/go.mod h1:bwPIfLdm/FzWgVUH8WPVlr+uJhscvNGFcaZKXsI3n2c=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
|
||||
github.com/go-xorm/xorm v0.7.1 h1:Kj7mfuqctPdX60zuxP6EoEut0f3E6K66H6hcoxiHUMc=
|
||||
github.com/go-xorm/xorm v0.7.1/go.mod h1:EHS1htMQFptzMaIHKyzqpHGw6C9Rtug75nsq6DA9unI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY=
|
||||
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 h1:kCvm3G3u+eTRbjfLPyfsfznJtraYEfZer/UvQ6CaQhI=
|
||||
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5/go.mod h1:6DM2KNNK69jRu0lAHmYK9LYxmqpNjYHOaNp/ZxttD4U=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/compress v1.5.0 h1:iDac0ZKbmSA4PRrRuXXjZL8C7UoJan8oBYxXkMzEQrI=
|
||||
github.com/klauspost/compress v1.5.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/ouqiang/goutil v1.1.1 h1:r5EKn1jw6vBnj0sLrk3XWeQGxG5ftjGQ3ftuiiO2DYE=
|
||||
github.com/ouqiang/goutil v1.1.1/go.mod h1:QrB1Ky4uGqcixxOx55MXweI3IA6nDZ0NtLMXbMfkur4=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs=
|
||||
github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/macaron.v1 v1.3.2 h1:AvWIaPmwBUA87/OWzePkoxeaw6YJWDfBt1pDFPBnLf8=
|
||||
gopkg.in/macaron.v1 v1.3.2/go.mod h1:PrsiawTWAGZs6wFbT5hlr7SQ2Ns9h7cUVtcUu4lQOVo=
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1,19 +0,0 @@
|
||||
// +build node
|
||||
// 任务节点
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/modules/rpc/server"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var addr string
|
||||
if (len(os.Args) < 2) {
|
||||
addr = "0.0.0.0:5921"
|
||||
} else {
|
||||
addr = os.Args[1]
|
||||
}
|
||||
server.Start(addr)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// +build gocron
|
||||
// 调度中心
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
|
||||
"github.com/ouqiang/gocron/cmd"
|
||||
)
|
||||
|
||||
const AppVersion = "0.4"
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "gocron"
|
||||
app.Usage = "gocron service"
|
||||
app.Version = AppVersion
|
||||
app.Commands = []cli.Command{
|
||||
cmd.CmdWeb,
|
||||
cmd.CmdServ,
|
||||
}
|
||||
app.Flags = append(app.Flags, []cli.Flag{}...)
|
||||
app.Run(os.Args)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
// 主机
|
||||
type Host struct {
|
||||
Id int16 `json:"id" xorm:"smallint pk autoincr"`
|
||||
Name string `json:"name" xorm:"varchar(64) notnull"` // 主机名称
|
||||
Alias string `json:"alias" xorm:"varchar(32) notnull default '' "` // 主机别名
|
||||
Port int `json:"port" xorm:"notnull default 5921"` // 主机端口
|
||||
Remark string `json:"remark" xorm:"varchar(100) notnull default '' "` // 备注
|
||||
BaseModel `json:"-" xorm:"-"`
|
||||
Selected bool `json:"-" xorm:"-"`
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (host *Host) Create() (insertId int16, err error) {
|
||||
_, err = Db.Insert(host)
|
||||
if err == nil {
|
||||
insertId = host.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (host *Host) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(new(Host))
|
||||
}
|
||||
|
||||
func (host *Host) Find(id int) error {
|
||||
_, err := Db.Id(id).Get(host)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (host *Host) AllList() ([]Host, error) {
|
||||
list := make([]Host, 0)
|
||||
err := Db.Cols("name,port").Desc("id").Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (host *Host) Total(params CommonMap) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 用户登录日志
|
||||
|
||||
type LoginLog struct {
|
||||
Id int `json:"id" xorm:"pk autoincr notnull "`
|
||||
Username string `json:"username" xorm:"varchar(32) notnull"`
|
||||
Ip string `json:"ip" xorm:"varchar(15) not null"`
|
||||
Created time.Time `json:"created" xorm:"datetime notnull created"`
|
||||
BaseModel `json:"-" xorm:"-"`
|
||||
}
|
||||
|
||||
func (log *LoginLog) Create() (insertId int, err error) {
|
||||
_, err = Db.Insert(log)
|
||||
if err == nil {
|
||||
insertId = log.Id
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (log *LoginLog) Total() (int64, error) {
|
||||
return Db.Count(log)
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
)
|
||||
|
||||
type Migration struct{}
|
||||
|
||||
// 首次安装, 创建数据库表
|
||||
func (migration *Migration) Install(dbName string) error {
|
||||
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()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 迭代升级数据库, 新建表、新增字段等
|
||||
func (migration *Migration) Upgrade(oldVersionId int) {
|
||||
// v1.2版本不支持升级
|
||||
if oldVersionId == 120 {
|
||||
return
|
||||
}
|
||||
|
||||
versionIds := []int{110, 122, 130, 140, 150}
|
||||
upgradeFuncs := []func(*xorm.Session) error{
|
||||
migration.upgradeFor110,
|
||||
migration.upgradeFor122,
|
||||
migration.upgradeFor130,
|
||||
migration.upgradeFor140,
|
||||
migration.upgradeFor150,
|
||||
}
|
||||
|
||||
startIndex := -1
|
||||
// 从当前版本的下一版本开始升级
|
||||
for i, value := range versionIds {
|
||||
if value > oldVersionId {
|
||||
startIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if startIndex == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
length := len(versionIds)
|
||||
if startIndex >= length {
|
||||
return
|
||||
}
|
||||
|
||||
session := Db.NewSession()
|
||||
err := session.Begin()
|
||||
if err != nil {
|
||||
logger.Fatalf("开启事务失败-%s", err.Error())
|
||||
}
|
||||
for startIndex < length {
|
||||
err = upgradeFuncs[startIndex](session)
|
||||
if err == nil {
|
||||
startIndex++
|
||||
continue
|
||||
}
|
||||
dbErr := session.Rollback()
|
||||
if dbErr != nil {
|
||||
logger.Fatalf("事务回滚失败-%s", dbErr.Error())
|
||||
}
|
||||
logger.Fatal(err)
|
||||
}
|
||||
err = session.Commit()
|
||||
if err != nil {
|
||||
logger.Fatalf("提交事务失败-%s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 升级到v1.1版本
|
||||
func (migration *Migration) upgradeFor110(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.1")
|
||||
// 创建表task_host
|
||||
err := session.Sync2(new(TaskHost))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tableName := TablePrefix + "task"
|
||||
// 把task对应的host_id写入task_host表
|
||||
sql := fmt.Sprintf("SELECT id, host_id FROM %s WHERE host_id > 0", tableName)
|
||||
results, err := session.Query(sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, value := range results {
|
||||
taskHostModel := &TaskHost{}
|
||||
taskId, err := strconv.Atoi(string(value["id"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostId, err := strconv.Atoi(string(value["host_id"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskHostModel.TaskId = taskId
|
||||
taskHostModel.HostId = int16(hostId)
|
||||
_, err = session.Insert(taskHostModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 删除task表host_id字段
|
||||
_, err = session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN host_id", tableName))
|
||||
|
||||
logger.Info("已升级到v1.1\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 升级到1.2.2版本
|
||||
func (migration *Migration) upgradeFor122(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.2.2")
|
||||
|
||||
tableName := TablePrefix + "task"
|
||||
// task表增加tag字段
|
||||
_, err := session.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN tag VARCHAR(32) NOT NULL DEFAULT '' ", tableName))
|
||||
|
||||
logger.Info("已升级到v1.2.2\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 升级到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
|
||||
}
|
||||
|
||||
func (m *Migration) upgradeFor150(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.5")
|
||||
|
||||
tableName := TablePrefix + "task"
|
||||
// task表增加字段 notify_keyword
|
||||
sql := fmt.Sprintf(
|
||||
"ALTER TABLE %s ADD COLUMN notify_keyword VARCHAR(128) NOT NULL DEFAULT '' ", tableName)
|
||||
_, err := session.Exec(sql)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settingModel := new(Setting)
|
||||
settingModel.Code = MailCode
|
||||
settingModel.Key = MailTemplateKey
|
||||
settingModel.Value = emailTemplate
|
||||
_, err = Db.Insert(settingModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settingModel.Id = 0
|
||||
settingModel.Code = SlackCode
|
||||
settingModel.Key = SlackTemplateKey
|
||||
settingModel.Value = slackTemplate
|
||||
_, err = Db.Insert(settingModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settingModel.Id = 0
|
||||
settingModel.Code = WebhookCode
|
||||
settingModel.Key = WebhookUrlKey
|
||||
settingModel.Value = ""
|
||||
_, err = Db.Insert(settingModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settingModel.Id = 0
|
||||
settingModel.Code = WebhookCode
|
||||
settingModel.Key = WebhookTemplateKey
|
||||
settingModel.Value = webhookTemplate
|
||||
_, err = Db.Insert(settingModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("已升级到v1.5\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/setting"
|
||||
)
|
||||
|
||||
type Status int8
|
||||
type CommonMap map[string]interface{}
|
||||
|
||||
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 // 取消
|
||||
)
|
||||
|
||||
const (
|
||||
Page = 1 // 当前页数
|
||||
PageSize = 20 // 每页多少条数据
|
||||
MaxPageSize = 1000 // 每次最多取多少条
|
||||
)
|
||||
|
||||
const DefaultTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
const (
|
||||
dbPingInterval = 90 * time.Second
|
||||
dbMaxLiftTime = 2 * time.Hour
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (model *BaseModel) pageLimitOffset() int {
|
||||
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)
|
||||
engine.SetConnMaxLifetime(dbMaxLiftTime)
|
||||
|
||||
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)
|
||||
|
||||
return engine
|
||||
}
|
||||
|
||||
// 创建临时数据库连接
|
||||
func CreateTmpDb(setting *setting.Setting) (*xorm.Engine, error) {
|
||||
dsn := getDbEngineDSN(setting)
|
||||
|
||||
return xorm.NewEngine(setting.Db.Engine, dsn)
|
||||
}
|
||||
|
||||
// 获取数据库引擎DSN mysql,sqlite,postgres
|
||||
func getDbEngineDSN(setting *setting.Setting) string {
|
||||
engine := strings.ToLower(setting.Db.Engine)
|
||||
dsn := ""
|
||||
switch engine {
|
||||
case "mysql":
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&allowNativePasswords=true",
|
||||
setting.Db.User,
|
||||
setting.Db.Password,
|
||||
setting.Db.Host,
|
||||
setting.Db.Port,
|
||||
setting.Db.Database,
|
||||
setting.Db.Charset)
|
||||
case "postgres":
|
||||
dsn = fmt.Sprintf("user=%s password=%s host=%s port=%d dbname=%s sslmode=disable",
|
||||
setting.Db.User,
|
||||
setting.Db.Password,
|
||||
setting.Db.Host,
|
||||
setting.Db.Port,
|
||||
setting.Db.Database)
|
||||
}
|
||||
|
||||
return dsn
|
||||
}
|
||||
|
||||
func keepDbAlived(engine *xorm.Engine) {
|
||||
t := time.Tick(dbPingInterval)
|
||||
var err error
|
||||
for {
|
||||
<-t
|
||||
err = engine.Ping()
|
||||
if err != nil {
|
||||
logger.Infof("database ping: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"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 '' "`
|
||||
}
|
||||
|
||||
const slackTemplate = `
|
||||
任务ID: {{.TaskId}}
|
||||
任务名称: {{.TaskName}}
|
||||
状态: {{.Status}}
|
||||
执行结果: {{.Result}}
|
||||
备注: {{.Remark}}
|
||||
`
|
||||
const emailTemplate = `
|
||||
任务ID: {{.TaskId}}
|
||||
任务名称: {{.TaskName}}
|
||||
状态: {{.Status}}
|
||||
执行结果: {{.Result}}
|
||||
备注: {{.Remark}}
|
||||
`
|
||||
const webhookTemplate = `
|
||||
{
|
||||
"task_id": "{{.TaskId}}",
|
||||
"task_name": "{{.TaskName}}",
|
||||
"status": "{{.Status}}",
|
||||
"result": "{{.Result}}",
|
||||
"remark": "{{.Remark}}"
|
||||
}
|
||||
`
|
||||
|
||||
const (
|
||||
SlackCode = "slack"
|
||||
SlackUrlKey = "url"
|
||||
SlackTemplateKey = "template"
|
||||
SlackChannelKey = "channel"
|
||||
)
|
||||
|
||||
const (
|
||||
MailCode = "mail"
|
||||
MailTemplateKey = "template"
|
||||
MailServerKey = "server"
|
||||
MailUserKey = "user"
|
||||
)
|
||||
|
||||
const (
|
||||
WebhookCode = "webhook"
|
||||
WebhookTemplateKey = "template"
|
||||
WebhookUrlKey = "url"
|
||||
)
|
||||
|
||||
// 初始化基本字段 邮件、slack等
|
||||
func (setting *Setting) InitBasicField() {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackUrlKey
|
||||
setting.Value = ""
|
||||
Db.Insert(setting)
|
||||
setting.Id = 0
|
||||
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackTemplateKey
|
||||
setting.Value = slackTemplate
|
||||
Db.Insert(setting)
|
||||
setting.Id = 0
|
||||
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailServerKey
|
||||
setting.Value = ""
|
||||
Db.Insert(setting)
|
||||
setting.Id = 0
|
||||
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailTemplateKey
|
||||
setting.Value = emailTemplate
|
||||
Db.Insert(setting)
|
||||
setting.Id = 0
|
||||
|
||||
setting.Code = WebhookCode
|
||||
setting.Key = WebhookTemplateKey
|
||||
setting.Value = webhookTemplate
|
||||
Db.Insert(setting)
|
||||
setting.Id = 0
|
||||
|
||||
setting.Code = WebhookCode
|
||||
setting.Key = WebhookUrlKey
|
||||
setting.Value = ""
|
||||
Db.Insert(setting)
|
||||
}
|
||||
|
||||
// region slack配置
|
||||
|
||||
type Slack struct {
|
||||
Url string `json:"url"`
|
||||
Channels []Channel `json:"channels"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (setting *Setting) Slack() (Slack, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", SlackCode).Find(&list)
|
||||
slack := Slack{}
|
||||
if err != nil {
|
||||
return slack, err
|
||||
}
|
||||
|
||||
setting.formatSlack(list, &slack)
|
||||
|
||||
return slack, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatSlack(list []Setting, slack *Slack) {
|
||||
for _, v := range list {
|
||||
switch v.Key {
|
||||
case SlackUrlKey:
|
||||
slack.Url = v.Value
|
||||
case SlackTemplateKey:
|
||||
slack.Template = v.Value
|
||||
default:
|
||||
slack.Channels = append(slack.Channels, Channel{
|
||||
v.Id, v.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (setting *Setting) UpdateSlack(url, template string) error {
|
||||
setting.Value = url
|
||||
|
||||
Db.Cols("value").Update(setting, Setting{Code: SlackCode, Key: SlackUrlKey})
|
||||
|
||||
setting.Value = template
|
||||
Db.Cols("value").Update(setting, Setting{Code: SlackCode, Key: SlackTemplateKey})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建slack渠道
|
||||
func (setting *Setting) CreateChannel(channel string) (int64, error) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
return Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) IsChannelExist(channel string) bool {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
count, _ := Db.Count(setting)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
type Mail struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
MailUsers []MailUser `json:"mail_users"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
type MailUser struct {
|
||||
Id int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
setting.formatMail(list, &mail)
|
||||
|
||||
return mail, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatMail(list []Setting, mail *Mail) {
|
||||
mailUser := MailUser{}
|
||||
for _, v := range list {
|
||||
switch v.Key {
|
||||
case MailServerKey:
|
||||
json.Unmarshal([]byte(v.Value), mail)
|
||||
case MailUserKey:
|
||||
json.Unmarshal([]byte(v.Value), &mailUser)
|
||||
mailUser.Id = v.Id
|
||||
mail.MailUsers = append(mail.MailUsers, mailUser)
|
||||
case MailTemplateKey:
|
||||
mail.Template = v.Value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (setting *Setting) UpdateMail(config, template string) error {
|
||||
setting.Value = config
|
||||
Db.Cols("value").Update(setting, Setting{Code: MailCode, Key: MailServerKey})
|
||||
|
||||
setting.Value = template
|
||||
Db.Cols("value").Update(setting, Setting{Code: MailCode, Key: MailTemplateKey})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type WebHook struct {
|
||||
Url string `json:"url"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
func (setting *Setting) Webhook() (WebHook, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", WebhookCode).Find(&list)
|
||||
webHook := WebHook{}
|
||||
if err != nil {
|
||||
return webHook, err
|
||||
}
|
||||
|
||||
setting.formatWebhook(list, &webHook)
|
||||
|
||||
return webHook, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatWebhook(list []Setting, webHook *WebHook) {
|
||||
for _, v := range list {
|
||||
switch v.Key {
|
||||
case WebhookUrlKey:
|
||||
webHook.Url = v.Value
|
||||
case WebhookTemplateKey:
|
||||
webHook.Template = v.Value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (setting *Setting) UpdateWebHook(url, template string) error {
|
||||
setting.Value = url
|
||||
|
||||
Db.Cols("value").Update(setting, Setting{Code: WebhookCode, Key: WebhookUrlKey})
|
||||
|
||||
setting.Value = template
|
||||
Db.Cols("value").Update(setting, Setting{Code: WebhookCode, Key: WebhookTemplateKey})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,276 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type TaskProtocol int8
|
||||
|
||||
const (
|
||||
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
|
||||
TaskRPC // RPC方式执行命令
|
||||
)
|
||||
|
||||
type TaskLevel int8
|
||||
|
||||
const (
|
||||
TaskLevelParent TaskLevel = 1 // 父任务
|
||||
TaskLevelChild TaskLevel = 2 // 子任务(依赖任务)
|
||||
)
|
||||
|
||||
type TaskDependencyStatus int8
|
||||
|
||||
const (
|
||||
TaskDependencyStatusStrong TaskDependencyStatus = 1 // 强依赖
|
||||
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
|
||||
)
|
||||
|
||||
type TaskHTTPMethod int8
|
||||
|
||||
const (
|
||||
TaskHTTPMethodGet TaskHTTPMethod = 1
|
||||
TaskHttpMethodPost TaskHTTPMethod = 2
|
||||
)
|
||||
|
||||
// 任务
|
||||
type Task struct {
|
||||
Id int `json:"id" xorm:"int pk autoincr"`
|
||||
Name string `json:"name" xorm:"varchar(32) notnull"` // 任务名称
|
||||
Level TaskLevel `json:"level" xorm:"tinyint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
|
||||
DependencyTaskId string `json:"dependency_task_id" xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
|
||||
DependencyStatus TaskDependencyStatus `json:"dependency_status" xorm:"tinyint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
|
||||
Spec string `json:"spec" xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `json:"protocol" xorm:"tinyint notnull index"` // 协议 1:http 2:系统命令
|
||||
Command string `json:"command" xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
HttpMethod TaskHTTPMethod `json:"http_method" xorm:"tinyint notnull default 1"` // http请求方法
|
||||
Timeout int `json:"timeout" xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
Multi int8 `json:"multi" xorm:"tinyint notnull default 1"` // 是否允许多实例运行
|
||||
RetryTimes int8 `json:"retry_times" xorm:"tinyint notnull default 0"` // 重试次数
|
||||
RetryInterval int16 `json:"retry_interval" xorm:"smallint notnull default 0"` // 重试间隔时间
|
||||
NotifyStatus int8 `json:"notify_status" xorm:"tinyint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知 3: 任务执行结果关键字匹配通知
|
||||
NotifyType int8 `json:"notify_type" xorm:"tinyint notnull default 0"` // 通知类型 1: 邮件 2: slack 3: webhook
|
||||
NotifyReceiverId string `json:"notify_receiver_id" xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
||||
NotifyKeyword string `json:"notify_keyword" xorm:"varchar(128) notnull default '' "`
|
||||
Tag string `json:"tag" xorm:"varchar(32) notnull default ''"`
|
||||
Remark string `json:"remark" xorm:"varchar(100) notnull default ''"` // 备注
|
||||
Status Status `json:"status" xorm:"tinyint notnull index default 0"` // 状态 1:正常 0:停止
|
||||
Created time.Time `json:"created" xorm:"datetime notnull created"` // 创建时间
|
||||
Deleted time.Time `json:"deleted" xorm:"datetime deleted"` // 删除时间
|
||||
BaseModel `json:"-" xorm:"-"`
|
||||
Hosts []TaskHostDetail `json:"hosts" xorm:"-"`
|
||||
NextRunTime time.Time `json:"next_run_time" xorm:"-"`
|
||||
}
|
||||
|
||||
func taskHostTableName() []string {
|
||||
return []string{TablePrefix + "task_host", "th"}
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (task *Task) Create() (insertId int, err error) {
|
||||
_, err = Db.Insert(task)
|
||||
if err == nil {
|
||||
insertId = task.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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, notify_keyword`).
|
||||
Update(task)
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (task *Task) Update(id int, data CommonMap) (int64, error) {
|
||||
return Db.Table(task).ID(id).Update(data)
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (task *Task) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(task)
|
||||
}
|
||||
|
||||
// 禁用
|
||||
func (task *Task) Disable(id int) (int64, error) {
|
||||
return task.Update(id, CommonMap{"status": Disabled})
|
||||
}
|
||||
|
||||
// 激活
|
||||
func (task *Task) Enable(id int) (int64, error) {
|
||||
return task.Update(id, CommonMap{"status": Enabled})
|
||||
}
|
||||
|
||||
// 获取所有激活任务
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if len(taskIds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func (task *Task) setHostsForTasks(tasks []Task) ([]Task, error) {
|
||||
taskHostModel := new(TaskHost)
|
||||
var err error
|
||||
for i, value := range tasks {
|
||||
taskHostDetails, err := taskHostModel.GetHostIdsByTaskId(value.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tasks[i].Hosts = taskHostDetails
|
||||
}
|
||||
|
||||
return 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)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return task.Status, nil
|
||||
}
|
||||
|
||||
func (task *Task) Detail(id int) (Task, error) {
|
||||
t := Task{}
|
||||
_, err := Db.Where("id=?", id).Get(&t)
|
||||
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
taskHostModel := new(TaskHost)
|
||||
t.Hosts, err = taskHostModel.GetHostIdsByTaskId(id)
|
||||
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (task *Task) List(params CommonMap) ([]Task, error) {
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]Task, 0)
|
||||
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
|
||||
task.parseWhere(session, params)
|
||||
err := session.GroupBy("t.id").Desc("t.id").Cols("t.*").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return task.setHostsForTasks(list)
|
||||
}
|
||||
|
||||
// 获取依赖任务列表
|
||||
func (task *Task) GetDependencyTaskList(ids string) ([]Task, error) {
|
||||
list := make([]Task, 0)
|
||||
if ids == "" {
|
||||
return list, nil
|
||||
}
|
||||
idList := strings.Split(ids, ",")
|
||||
taskIds := make([]interface{}, len(idList))
|
||||
for i, v := range idList {
|
||||
taskIds[i] = v
|
||||
}
|
||||
fields := "t.*"
|
||||
err := Db.Alias("t").
|
||||
Where("t.level = ?", TaskLevelChild).
|
||||
In("t.id", taskIds).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
return task.setHostsForTasks(list)
|
||||
}
|
||||
|
||||
func (task *Task) Total(params CommonMap) (int64, error) {
|
||||
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
|
||||
task.parseWhere(session, params)
|
||||
list := make([]Task, 0)
|
||||
|
||||
err := session.GroupBy("t.id").Find(&list)
|
||||
|
||||
return int64(len(list)), err
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
id, ok := params["Id"]
|
||||
if ok && id.(int) > 0 {
|
||||
session.And("t.id = ?", id)
|
||||
}
|
||||
hostId, ok := params["HostId"]
|
||||
if ok && hostId.(int) > 0 {
|
||||
session.And("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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package models
|
||||
|
||||
type TaskHost struct {
|
||||
Id int `json:"id" xorm:"int pk autoincr"`
|
||||
TaskId int `json:"task_id" xorm:"int not null index"`
|
||||
HostId int16 `json:"host_id" xorm:"smallint not null index"`
|
||||
}
|
||||
|
||||
type TaskHostDetail struct {
|
||||
TaskHost `xorm:"extends"`
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
|
||||
func (TaskHostDetail) TableName() string {
|
||||
return TablePrefix + "task_host"
|
||||
}
|
||||
|
||||
func hostTableName() []string {
|
||||
return []string{TablePrefix + "host", "h"}
|
||||
}
|
||||
|
||||
func (th *TaskHost) Remove(taskId int) error {
|
||||
_, err := Db.Where("task_id = ?", taskId).Delete(new(TaskHost))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (th *TaskHost) Add(taskId int, hostIds []int) error {
|
||||
|
||||
err := th.Remove(taskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskHosts := make([]TaskHost, len(hostIds))
|
||||
for i, value := range hostIds {
|
||||
taskHosts[i].TaskId = taskId
|
||||
taskHosts[i].HostId = int16(value)
|
||||
}
|
||||
|
||||
_, err = Db.Insert(&taskHosts)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (th *TaskHost) GetHostIdsByTaskId(taskId int) ([]TaskHostDetail, error) {
|
||||
list := make([]TaskHostDetail, 0)
|
||||
fields := "th.id,th.host_id,h.alias,h.name,h.port"
|
||||
err := Db.Alias("th").
|
||||
Join("LEFT", hostTableName(), "th.host_id=h.id").
|
||||
Where("th.task_id = ?", taskId).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (th *TaskHost) GetTaskIdsByHostId(hostId int16) ([]interface{}, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
err := Db.Where("host_id = ?", hostId).Cols("task_id").Find(&list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskIds := make([]interface{}, len(list))
|
||||
for i, value := range list {
|
||||
taskIds[i] = value.TaskId
|
||||
}
|
||||
|
||||
return taskIds, err
|
||||
}
|
||||
|
||||
// 判断主机id是否有引用
|
||||
func (th *TaskHost) HostIdExist(hostId int16) (bool, error) {
|
||||
count, err := Db.Where("host_id = ?", hostId).Count(th)
|
||||
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type TaskType int8
|
||||
|
||||
// 任务执行日志
|
||||
type TaskLog struct {
|
||||
Id int64 `json:"id" xorm:"bigint pk autoincr"`
|
||||
TaskId int `json:"task_id" xorm:"int notnull index default 0"` // 任务id
|
||||
Name string `json:"name" xorm:"varchar(32) notnull"` // 任务名称
|
||||
Spec string `json:"spec" xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `json:"protocol" xorm:"tinyint notnull index"` // 协议 1:http 2:RPC
|
||||
Command string `json:"command" xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
Timeout int `json:"timeout" xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
RetryTimes int8 `json:"retry_times" xorm:"tinyint notnull default 0"` // 任务重试次数
|
||||
Hostname string `json:"hostname" xorm:"varchar(128) notnull default '' "` // RPC主机名,逗号分隔
|
||||
StartTime time.Time `json:"start_time" xorm:"datetime created"` // 开始执行时间
|
||||
EndTime time.Time `json:"end_time" xorm:"datetime updated"` // 执行完成(失败)时间
|
||||
Status Status `json:"status" xorm:"tinyint notnull index default 1"` // 状态 0:执行失败 1:执行中 2:执行完毕 3:任务取消(上次任务未执行完成) 4:异步执行
|
||||
Result string `json:"result" xorm:"mediumtext notnull "` // 执行结果
|
||||
TotalTime int `json:"total_time" xorm:"-"` // 执行总时长
|
||||
BaseModel `json:"-" xorm:"-"`
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Create() (insertId int64, err error) {
|
||||
_, err = Db.Insert(taskLog)
|
||||
if err == nil {
|
||||
insertId = taskLog.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (taskLog *TaskLog) Update(id int64, data CommonMap) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 清空表
|
||||
func (taskLog *TaskLog) Clear() (int64, error) {
|
||||
return Db.Where("1=1").Delete(taskLog)
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func (taskLog *TaskLog) Remove(id int) (int64, error) {
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Total(params CommonMap) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
)
|
||||
|
||||
const PasswordSaltLength = 6
|
||||
|
||||
// 用户model
|
||||
type User struct {
|
||||
Id int `json:"id" xorm:"pk autoincr notnull "`
|
||||
Name string `json:"name" xorm:"varchar(32) notnull unique"` // 用户名
|
||||
Password string `json:"-" xorm:"char(32) notnull "` // 密码
|
||||
Salt string `json:"-" xorm:"char(6) notnull "` // 密码盐值
|
||||
Email string `json:"email" xorm:"varchar(50) notnull unique default '' "` // 邮箱
|
||||
Created time.Time `json:"created" xorm:"datetime notnull created"`
|
||||
Updated time.Time `json:"updated" xorm:"datetime updated"`
|
||||
IsAdmin int8 `json:"is_admin" xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户
|
||||
Status Status `json:"status" xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用
|
||||
BaseModel `json:"-" xorm:"-"`
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (user *User) Create() (insertId int, err error) {
|
||||
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
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (user *User) Update(id int, data CommonMap) (int64, error) {
|
||||
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)
|
||||
|
||||
return user.Update(id, CommonMap{"password": safePassword, "salt": salt})
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (user *User) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(user)
|
||||
}
|
||||
|
||||
// 禁用
|
||||
func (user *User) Disable(id int) (int64, error) {
|
||||
return user.Update(id, CommonMap{"status": Disabled})
|
||||
}
|
||||
|
||||
// 激活
|
||||
func (user *User) Enable(id int) (int64, error) {
|
||||
return user.Update(id, CommonMap{"status": Enabled})
|
||||
}
|
||||
|
||||
// 验证用户名和密码
|
||||
func (user *User) Match(username, password string) bool {
|
||||
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)
|
||||
|
||||
return hashPassword == user.Password
|
||||
}
|
||||
|
||||
// 获取用户详情
|
||||
func (user *User) Find(id int) error {
|
||||
_, err := Db.Id(id).Get(user)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 用户名是否存在
|
||||
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, 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(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
|
||||
}
|
||||
|
||||
func (user *User) Total() (int64, error) {
|
||||
return Db.Count(user)
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
func (user *User) encryptPassword(password, salt string) string {
|
||||
return utils.Md5(password + salt)
|
||||
}
|
||||
|
||||
// 生成密码盐值
|
||||
func (user *User) generateSalt() string {
|
||||
return utils.RandString(PasswordSaltLength)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/setting"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/goutil"
|
||||
)
|
||||
|
||||
var (
|
||||
// AppDir 应用根目录
|
||||
AppDir string // 应用根目录
|
||||
// ConfDir 配置文件目录
|
||||
ConfDir string // 配置目录
|
||||
// LogDir 日志目录
|
||||
LogDir string // 日志目录
|
||||
// AppConfig 配置文件
|
||||
AppConfig string // 应用配置文件
|
||||
// Installed 应用是否已安装
|
||||
Installed bool // 应用是否安装过
|
||||
// Setting 应用配置
|
||||
Setting *setting.Setting // 应用配置
|
||||
// VersionId 版本号
|
||||
VersionId int // 版本号
|
||||
// VersionFile 版本文件
|
||||
VersionFile string // 版本号文件
|
||||
)
|
||||
|
||||
// InitEnv 初始化
|
||||
func InitEnv(versionString string) {
|
||||
logger.InitLogger()
|
||||
var err error
|
||||
AppDir, err = goutil.WorkDir()
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
ConfDir = filepath.Join(AppDir, "/conf")
|
||||
LogDir = filepath.Join(AppDir, "/log")
|
||||
AppConfig = filepath.Join(ConfDir, "/app.ini")
|
||||
VersionFile = filepath.Join(ConfDir, "/.version")
|
||||
createDirIfNotExists(ConfDir, LogDir)
|
||||
Installed = IsInstalled()
|
||||
VersionId = ToNumberVersion(versionString)
|
||||
}
|
||||
|
||||
// IsInstalled 判断应用是否已安装
|
||||
func IsInstalled() bool {
|
||||
_, err := os.Stat(filepath.Join(ConfDir, "/install.lock"))
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateInstallLock 创建安装锁文件
|
||||
func CreateInstallLock() error {
|
||||
_, err := os.Create(filepath.Join(ConfDir, "/install.lock"))
|
||||
if err != nil {
|
||||
logger.Error("创建安装锁文件conf/install.lock失败")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateVersionFile 更新应用版本号文件
|
||||
func UpdateVersionFile() {
|
||||
err := ioutil.WriteFile(VersionFile,
|
||||
[]byte(strconv.Itoa(VersionId)),
|
||||
0644,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentVersionId 获取应用当前版本号, 从版本号文件中读取
|
||||
func GetCurrentVersionId() int {
|
||||
if !utils.FileExist(VersionFile) {
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes, err := ioutil.ReadFile(VersionFile)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
versionId, err := strconv.Atoi(strings.TrimSpace(string(bytes)))
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
return versionId
|
||||
}
|
||||
|
||||
// ToNumberVersion 把字符串版本号a.b.c转换为整数版本号abc
|
||||
func ToNumberVersion(versionString string) int {
|
||||
versionString = strings.TrimPrefix(versionString, "v")
|
||||
v := strings.Replace(versionString, ".", "", -1)
|
||||
if len(v) < 3 {
|
||||
v += "0"
|
||||
}
|
||||
|
||||
versionId, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
return versionId
|
||||
}
|
||||
|
||||
// 检测目录是否存在
|
||||
func 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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package httpclient
|
||||
|
||||
// http-client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
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)}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/cihub/seelog"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// 日志库
|
||||
|
||||
type Level int8
|
||||
|
||||
var logger seelog.LoggerInterface
|
||||
|
||||
const (
|
||||
DEBUG = iota
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
FATAL
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
write(DEBUG, 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 Warn(v ...interface{}) {
|
||||
write(WARN, 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 Fatal(v ...interface{}) {
|
||||
write(FATAL, v)
|
||||
}
|
||||
|
||||
func Fatalf(format string, v ...interface{}) {
|
||||
writef(FATAL, format, v...)
|
||||
}
|
||||
|
||||
func write(level Level, 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)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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 {
|
||||
config := `
|
||||
<seelog>
|
||||
<outputs formatid="main">
|
||||
%s
|
||||
<filter levels="info,critical,error,warn">
|
||||
<file path="log/cron.log" />
|
||||
</filter>
|
||||
</outputs>
|
||||
<formats>
|
||||
<format id="main" format="%%Date/%%Time [%%LEV] %%Msg%%n"/>
|
||||
</formats>
|
||||
</seelog>`
|
||||
|
||||
consoleConfig := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
consoleConfig =
|
||||
`
|
||||
<filter levels="info,debug,critical,warn,error">
|
||||
<console />
|
||||
</filter>
|
||||
`
|
||||
}
|
||||
config = fmt.Sprintf(config, consoleConfig)
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gomail/gomail"
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/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
|
||||
}
|
||||
msg["content"] = parseNotifyTemplate(mailSetting.Template, msg)
|
||||
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.NewDialer(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)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
)
|
||||
|
||||
type Message map[string]interface{}
|
||||
|
||||
type Notifiable interface {
|
||||
Send(msg Message)
|
||||
}
|
||||
|
||||
var queue = make(chan Message, 100)
|
||||
|
||||
func init() {
|
||||
go run()
|
||||
}
|
||||
|
||||
// 把消息推入队列
|
||||
func Push(msg Message) {
|
||||
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)
|
||||
case 3:
|
||||
// WebHook
|
||||
webHook := WebHook{}
|
||||
go webHook.Send(msg)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func parseNotifyTemplate(notifyTemplate string, msg Message) string {
|
||||
tmpl, err := template.New("notify").Parse(notifyTemplate)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("解析通知模板失败: %s", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, map[string]interface{}{
|
||||
"TaskId": msg["task_id"],
|
||||
"TaskName": msg["name"],
|
||||
"Status": msg["status"],
|
||||
"Result": msg["output"],
|
||||
"Remark": msg["remark"],
|
||||
})
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package notify
|
||||
|
||||
// 发送消息到slack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
)
|
||||
|
||||
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)
|
||||
msg["content"] = parseNotifyTemplate(slackSetting.Template, msg)
|
||||
msg["content"] = html.UnescapeString(msg["content"].(string))
|
||||
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) 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
|
||||
}
|
||||
|
||||
// 格式化消息内容
|
||||
func (slack *Slack) format(content string, channel string) string {
|
||||
content = utils.EscapeJson(content)
|
||||
specialChars := []string{"&", "<", ">"}
|
||||
replaceChars := []string{"&", "<", ">"}
|
||||
content = utils.ReplaceStrings(content, specialChars, replaceChars)
|
||||
|
||||
return fmt.Sprintf(`{"text":"%s","username":"gocron", "channel":"%s"}`, content, channel)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"html"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
)
|
||||
|
||||
type WebHook struct{}
|
||||
|
||||
func (webHook *WebHook) Send(msg Message) {
|
||||
model := new(models.Setting)
|
||||
webHookSetting, err := model.Webhook()
|
||||
if err != nil {
|
||||
logger.Error("#webHook#从数据库获取webHook配置失败", err)
|
||||
return
|
||||
}
|
||||
if webHookSetting.Url == "" {
|
||||
logger.Error("#webHook#webhook-url为空")
|
||||
return
|
||||
}
|
||||
logger.Debugf("%+v", webHookSetting)
|
||||
msg["name"] = utils.EscapeJson(msg["name"].(string))
|
||||
msg["output"] = utils.EscapeJson(msg["output"].(string))
|
||||
msg["content"] = parseNotifyTemplate(webHookSetting.Template, msg)
|
||||
msg["content"] = html.UnescapeString(msg["content"].(string))
|
||||
webHook.send(msg, webHookSetting.Url)
|
||||
}
|
||||
|
||||
func (webHook *WebHook) send(msg Message, url string) {
|
||||
content := msg["content"].(string)
|
||||
timeout := 30
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
resp := httpclient.PostJson(url, content, timeout)
|
||||
if resp.StatusCode == 200 {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Errorf("webHook#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
type Certificate struct {
|
||||
CAFile string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
ServerName string
|
||||
}
|
||||
|
||||
func (c Certificate) GetTLSConfigForServer() (*tls.Config, error) {
|
||||
certificate, err := tls.LoadX509KeyPair(
|
||||
c.CertFile,
|
||||
c.KeyFile,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
bs, err := ioutil.ReadFile(c.CAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read client ca cert: %s", err)
|
||||
}
|
||||
|
||||
ok := certPool.AppendCertsFromPEM(bs)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to append client certs")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
ClientCAs: certPool,
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func (c Certificate) GetTransportCredsForClient() (credentials.TransportCredentials, error) {
|
||||
certificate, err := tls.LoadX509KeyPair(
|
||||
c.CertFile,
|
||||
c.KeyFile,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
bs, err := ioutil.ReadFile(c.CAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read ca cert: %s", err)
|
||||
}
|
||||
|
||||
ok := certPool.AppendCertsFromPEM(bs)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to append certs")
|
||||
}
|
||||
|
||||
transportCreds := credentials.NewTLS(&tls.Config{
|
||||
ServerName: c.ServerName,
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
RootCAs: certPool,
|
||||
})
|
||||
|
||||
return transportCreds, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/grpcpool"
|
||||
pb "github.com/ouqiang/gocron/internal/modules/rpc/proto"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
taskMap sync.Map
|
||||
)
|
||||
|
||||
var (
|
||||
errUnavailable = errors.New("无法连接远程服务器")
|
||||
)
|
||||
|
||||
func generateTaskUniqueKey(ip string, port int, id int64) string {
|
||||
return fmt.Sprintf("%s:%d:%d", ip, port, id)
|
||||
}
|
||||
|
||||
func Stop(ip string, port int, id int64) {
|
||||
key := generateTaskUniqueKey(ip, port, id)
|
||||
cancel, ok := taskMap.Load(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cancel.(context.CancelFunc)()
|
||||
}
|
||||
|
||||
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)
|
||||
c, err := grpcpool.Pool.Get(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if resp.Error == "" {
|
||||
return resp.Output, nil
|
||||
}
|
||||
|
||||
return resp.Output, errors.New(resp.Error)
|
||||
}
|
||||
|
||||
func parseGRPCError(err error) (string, error) {
|
||||
switch status.Code(err) {
|
||||
case codes.Unavailable:
|
||||
return "", errUnavailable
|
||||
case codes.DeadlineExceeded:
|
||||
return "", errors.New("执行超时, 强制结束")
|
||||
case codes.Canceled:
|
||||
return "", errors.New("手动停止")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package grpcpool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/auth"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
const (
|
||||
backOffMaxDelay = 3 * time.Second
|
||||
dialTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
Pool = &GRPCPool{
|
||||
conns: make(map[string]*Client),
|
||||
}
|
||||
|
||||
keepAliveParams = keepalive.ClientParameters{
|
||||
Time: 20 * time.Second,
|
||||
Timeout: 3 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
rpcClient rpc.TaskClient
|
||||
}
|
||||
|
||||
type GRPCPool struct {
|
||||
// map key格式 ip:port
|
||||
conns map[string]*Client
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *GRPCPool) Get(addr string) (rpc.TaskClient, error) {
|
||||
p.mu.RLock()
|
||||
client, ok := p.conns[addr]
|
||||
p.mu.RUnlock()
|
||||
if ok {
|
||||
return client.rpcClient, nil
|
||||
}
|
||||
|
||||
client, err := p.factory(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.rpcClient, nil
|
||||
}
|
||||
|
||||
// 释放连接
|
||||
func (p *GRPCPool) Release(addr string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
client, ok := p.conns[addr]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(p.conns, addr)
|
||||
client.conn.Close()
|
||||
}
|
||||
|
||||
// 创建连接
|
||||
func (p *GRPCPool) factory(addr string) (*Client, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
client, ok := p.conns[addr]
|
||||
if ok {
|
||||
return client, nil
|
||||
}
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithKeepaliveParams(keepAliveParams),
|
||||
grpc.WithBackoffMaxDelay(backOffMaxDelay),
|
||||
}
|
||||
|
||||
if !app.Setting.EnableTLS {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
} else {
|
||||
server := strings.Split(addr, ":")
|
||||
certificate := auth.Certificate{
|
||||
CAFile: app.Setting.CAFile,
|
||||
CertFile: app.Setting.CertFile,
|
||||
KeyFile: app.Setting.KeyFile,
|
||||
ServerName: server[0],
|
||||
}
|
||||
|
||||
transportCreds, err := certificate.GetTransportCredsForClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(transportCreds))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, addr, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client = &Client{
|
||||
conn: conn,
|
||||
rpcClient: rpc.NewTaskClient(conn),
|
||||
}
|
||||
|
||||
p.conns[addr] = client
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -9,6 +9,7 @@ service Task {
|
||||
message TaskRequest {
|
||||
string command = 2; // 命令
|
||||
int32 timeout = 3; // 任务执行超时时间
|
||||
int64 id = 4; // 执行任务唯一ID
|
||||
}
|
||||
|
||||
message TaskResponse {
|
||||
@@ -0,0 +1,96 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/auth"
|
||||
pb "github.com/ouqiang/gocron/internal/modules/rpc/proto"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
type Server struct{}
|
||||
|
||||
var keepAlivePolicy = keepalive.EnforcementPolicy{
|
||||
MinTime: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}
|
||||
|
||||
var keepAliveParams = keepalive.ServerParameters{
|
||||
MaxConnectionIdle: 30 * time.Second,
|
||||
Time: 30 * time.Second,
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
log.Infof("execute cmd start: [id: %d cmd: %s]", req.Id, req.Command)
|
||||
output, err := utils.ExecShell(ctx, req.Command)
|
||||
resp := new(pb.TaskResponse)
|
||||
resp.Output = output
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Error = ""
|
||||
}
|
||||
log.Infof("execute cmd end: [id: %d cmd: %s err: %s]", req.Id, req.Command, resp.Error)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func Start(addr string, enableTLS bool, certificate auth.Certificate) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
opts := []grpc.ServerOption{
|
||||
grpc.KeepaliveParams(keepAliveParams),
|
||||
grpc.KeepaliveEnforcementPolicy(keepAlivePolicy),
|
||||
}
|
||||
if enableTLS {
|
||||
tlsConfig, err := certificate.GetTLSConfigForServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
opt := grpc.Creds(credentials.NewTLS(tlsConfig))
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
server := grpc.NewServer(opts...)
|
||||
pb.RegisterTaskServer(server, Server{})
|
||||
log.Infof("server listen on %s", addr)
|
||||
|
||||
go func() {
|
||||
err = server.Serve(l)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
for {
|
||||
s := <-c
|
||||
log.Infoln("收到信号 -- ", s)
|
||||
switch s {
|
||||
case syscall.SIGHUP:
|
||||
log.Infoln("收到终端断开信号, 忽略")
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("应用准备退出")
|
||||
server.GracefulStop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/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
|
||||
|
||||
EnableTLS bool
|
||||
CAFile string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
|
||||
ConcurrencyQueue int
|
||||
AuthSecret string
|
||||
}
|
||||
|
||||
// 读取配置
|
||||
func Read(filename string) (*Setting, error) {
|
||||
config, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
section := config.Section(DefaultSection)
|
||||
|
||||
var s Setting
|
||||
|
||||
s.Db.Engine = section.Key("db.engine").MustString("mysql")
|
||||
s.Db.Host = section.Key("db.host").MustString("127.0.0.1")
|
||||
s.Db.Port = section.Key("db.port").MustInt(3306)
|
||||
s.Db.User = section.Key("db.user").MustString("")
|
||||
s.Db.Password = section.Key("db.password").MustString("")
|
||||
s.Db.Database = section.Key("db.database").MustString("gocron")
|
||||
s.Db.Prefix = section.Key("db.prefix").MustString("")
|
||||
s.Db.Charset = section.Key("db.charset").MustString("utf8")
|
||||
s.Db.MaxIdleConns = section.Key("db.max.idle.conns").MustInt(30)
|
||||
s.Db.MaxOpenConns = section.Key("db.max.open.conns").MustInt(100)
|
||||
|
||||
s.AllowIps = section.Key("allow_ips").MustString("")
|
||||
s.AppName = section.Key("app.name").MustString("定时任务管理系统")
|
||||
s.ApiKey = section.Key("api.key").MustString("")
|
||||
s.ApiSecret = section.Key("api.secret").MustString("")
|
||||
s.ApiSignEnable = section.Key("api.sign.enable").MustBool(true)
|
||||
s.ConcurrencyQueue = section.Key("concurrency.queue").MustInt(500)
|
||||
s.AuthSecret = section.Key("auth_secret").MustString("")
|
||||
if s.AuthSecret == "" {
|
||||
s.AuthSecret = utils.RandAuthToken()
|
||||
}
|
||||
|
||||
s.EnableTLS = section.Key("enable_tls").MustBool(false)
|
||||
s.CAFile = section.Key("ca_file").MustString("")
|
||||
s.CertFile = section.Key("cert_file").MustString("")
|
||||
s.KeyFile = section.Key("key_file").MustString("")
|
||||
|
||||
if s.EnableTLS {
|
||||
if !utils.FileExist(s.CAFile) {
|
||||
logger.Fatalf("failed to read ca cert file: %s", s.CAFile)
|
||||
}
|
||||
|
||||
if !utils.FileExist(s.CertFile) {
|
||||
logger.Fatalf("failed to read client cert file: %s", s.CertFile)
|
||||
}
|
||||
|
||||
if !utils.FileExist(s.KeyFile) {
|
||||
logger.Fatalf("failed to read client key file: %s", s.KeyFile)
|
||||
}
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// 写入配置
|
||||
func Write(config []string, filename string) error {
|
||||
if len(config) == 0 {
|
||||
return errors.New("参数不能为空")
|
||||
}
|
||||
if len(config)%2 != 0 {
|
||||
return errors.New("参数不匹配")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
)
|
||||
|
||||
// json 格式输出
|
||||
|
||||
type response struct {
|
||||
Code int `json:"code"` // 状态码 0:成功 非0:失败
|
||||
Message string `json:"message"` // 信息
|
||||
Data interface{} `json:"data"` // 数据
|
||||
}
|
||||
|
||||
type JsonResponse struct{}
|
||||
|
||||
const ResponseSuccess = 0
|
||||
const ResponseFailure = 1
|
||||
const UnauthorizedError = 403
|
||||
const AuthError = 401
|
||||
const NotFound = 404
|
||||
const ServerError = 500
|
||||
const AppNotInstall = 801
|
||||
|
||||
const SuccessContent = "操作成功"
|
||||
const FailureContent = "操作失败"
|
||||
|
||||
func JsonResponseByErr(err error) string {
|
||||
jsonResp := JsonResponse{}
|
||||
if err != nil {
|
||||
return jsonResp.CommonFailure(FailureContent, err)
|
||||
}
|
||||
|
||||
return jsonResp.Success(SuccessContent, nil)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Success(message string, data interface{}) string {
|
||||
return j.response(ResponseSuccess, message, data)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Failure(code int, message string) string {
|
||||
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) response(code int, message string, data interface{}) string {
|
||||
resp := response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
crand "crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tang-RoseChild/mahonia"
|
||||
)
|
||||
|
||||
func RandAuthToken() string {
|
||||
buf := make([]byte, 32)
|
||||
_, err := crand.Read(buf)
|
||||
if err != nil {
|
||||
return RandString(64)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", buf)
|
||||
}
|
||||
|
||||
// 生成长度为length的随机字符串
|
||||
func RandString(length int64) string {
|
||||
sources := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
var 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)
|
||||
}
|
||||
|
||||
// 生成32位MD5摘要
|
||||
func Md5(str string) string {
|
||||
m := md5.New()
|
||||
m.Write([]byte(str))
|
||||
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
// 生成0-max之间随机数
|
||||
func RandNumber(max int) int {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
return r.Intn(max)
|
||||
}
|
||||
|
||||
// GBK编码转换为UTF8
|
||||
func GBK2UTF8(s string) (string, bool) {
|
||||
dec := mahonia.NewDecoder("gbk")
|
||||
|
||||
return dec.ConvertStringOK(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)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func InStringSlice(slice []string, element string) bool {
|
||||
element = strings.TrimSpace(element)
|
||||
for _, v := range slice {
|
||||
if strings.TrimSpace(v) == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 转义json特殊字符
|
||||
func EscapeJson(s string) string {
|
||||
specialChars := []string{"\\", "\b", "\f", "\n", "\r", "\t", "\""}
|
||||
replaceChars := []string{"\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\""}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRandString(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandNumber(t *testing.T) {
|
||||
num := RandNumber(10000)
|
||||
if num <= 0 && num >= 10000 {
|
||||
t.Fatalf("随机数不在有效范围内-%d", num)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertEncoding(outputGBK string) string {
|
||||
// windows平台编码为gbk,需转换为utf8才能入库
|
||||
outputUTF8, ok := GBK2UTF8(outputGBK)
|
||||
if ok {
|
||||
return outputUTF8
|
||||
}
|
||||
|
||||
return outputGBK
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/client"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/grpcpool"
|
||||
"github.com/ouqiang/gocron/internal/modules/rpc/proto"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/routers/base"
|
||||
"github.com/ouqiang/gocron/internal/service"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
const testConnectionCommand = "echo hello"
|
||||
const testConnectionTimeout = 5
|
||||
|
||||
// Index 主机列表
|
||||
func Index(ctx *macaron.Context) string {
|
||||
hostModel := new(models.Host)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := hostModel.Total(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
hosts, err := hostModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, map[string]interface{}{
|
||||
"total": total,
|
||||
"data": hosts,
|
||||
})
|
||||
}
|
||||
|
||||
// All 获取所有主机
|
||||
func All(ctx *macaron.Context) string {
|
||||
hostModel := new(models.Host)
|
||||
hostModel.PageSize = -1
|
||||
hosts, err := hostModel.List(models.CommonMap{})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, hosts)
|
||||
}
|
||||
|
||||
// Detail 主机详情
|
||||
func Detail(ctx *macaron.Context) string {
|
||||
hostModel := new(models.Host)
|
||||
id := ctx.ParamsInt(":id")
|
||||
err := hostModel.Find(id)
|
||||
jsonResp := utils.JsonResponse{}
|
||||
if err != nil || hostModel.Id == 0 {
|
||||
logger.Errorf("获取主机详情失败#主机id-%d", id)
|
||||
return jsonResp.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, hostModel)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Error 表单验证错误处理
|
||||
func (f HostForm) Error(ctx *macaron.Context, errs binding.Errors) {
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
ctx.Write([]byte(content))
|
||||
}
|
||||
|
||||
// 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("主机不存在")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
taskModel := new(models.Task)
|
||||
tasks, err := taskModel.ActiveListByHostId(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("刷新任务主机信息失败", err)
|
||||
}
|
||||
service.ServiceTask.BatchAdd(tasks)
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
}
|
||||
|
||||
// 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("主机不存在")
|
||||
}
|
||||
|
||||
_, err = hostModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
|
||||
grpcpool.Pool.Release(addr)
|
||||
|
||||
return json.Success("操作成功", nil)
|
||||
}
|
||||
|
||||
// Ping 测试主机是否可连接
|
||||
func Ping(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
hostModel := new(models.Host)
|
||||
err := hostModel.Find(id)
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil || hostModel.Id <= 0 {
|
||||
return json.CommonFailure("主机不存在", err)
|
||||
}
|
||||
|
||||
taskReq := &rpc.TaskRequest{}
|
||||
taskReq.Command = testConnectionCommand
|
||||
taskReq.Timeout = testConnectionTimeout
|
||||
output, err := client.Exec(hostModel.Name, hostModel.Port, taskReq)
|
||||
if err != nil {
|
||||
return json.CommonFailure("连接失败-"+err.Error()+" "+output, err)
|
||||
}
|
||||
|
||||
return json.Success("连接成功", nil)
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
|
||||
var params = models.CommonMap{}
|
||||
params["Id"] = ctx.QueryInt("id")
|
||||
params["Name"] = ctx.QueryTrim("name")
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return params
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/lib/pq"
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/setting"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/service"
|
||||
)
|
||||
|
||||
// 系统安装
|
||||
|
||||
type InstallForm struct {
|
||||
DbType string `binding:"In(mysql,postgres)"`
|
||||
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("表单验证失败, 请检测输入")
|
||||
ctx.Write([]byte(content))
|
||||
}
|
||||
|
||||
// 安装
|
||||
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.Error())
|
||||
}
|
||||
// 写入数据库配置
|
||||
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
|
||||
|
||||
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 = app.CreateInstallLock()
|
||||
if err != nil {
|
||||
return json.CommonFailure("创建文件安装锁失败", err)
|
||||
}
|
||||
|
||||
// 更新版本号文件
|
||||
app.UpdateVersionFile()
|
||||
|
||||
app.Installed = true
|
||||
// 初始化定时任务
|
||||
service.ServiceTask.Initialize()
|
||||
|
||||
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", "5",
|
||||
"db.max.open.conns", "100",
|
||||
"allow_ips", "",
|
||||
"app.name", "定时任务管理系统", // 应用名称
|
||||
"api.key", "",
|
||||
"api.secret", "",
|
||||
"enable_tls", "false",
|
||||
"concurrency.queue", "500",
|
||||
"auth_secret", utils.RandAuthToken(),
|
||||
"ca_file", "",
|
||||
"cert_file", "",
|
||||
"key_file", "",
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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.Database = form.DbName
|
||||
s.Db.Charset = "utf8"
|
||||
db, err := models.CreateTmpDb(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
if s.Db.Engine == "postgres" && err != nil {
|
||||
pgError, ok := err.(*pq.Error)
|
||||
if ok && pgError.Code == "3D000" {
|
||||
err = errors.New("数据库不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Db.Engine == "mysql" && err != nil {
|
||||
mysqlError, ok := err.(*mysql.MySQLError)
|
||||
if ok && mysqlError.Number == 1049 {
|
||||
err = errors.New("数据库不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package loginlog
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/routers/base"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func Index(ctx *macaron.Context) string {
|
||||
loginLogModel := new(models.LoginLog)
|
||||
params := models.CommonMap{}
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
total, err := loginLogModel.Total()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
loginLogs, err := loginLogModel.List(params)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, map[string]interface{}{
|
||||
"total": total,
|
||||
"data": loginLogs,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package manage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func Slack(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
slack, err := settingModel.Slack()
|
||||
jsonResp := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return jsonResp.Success(utils.SuccessContent, nil)
|
||||
|
||||
}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, slack)
|
||||
}
|
||||
|
||||
func UpdateSlack(ctx *macaron.Context) string {
|
||||
url := ctx.QueryTrim("url")
|
||||
template := ctx.QueryTrim("template")
|
||||
settingModel := new(models.Setting)
|
||||
err := settingModel.UpdateSlack(url, template)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func CreateSlackChannel(ctx *macaron.Context) string {
|
||||
channel := ctx.QueryTrim("channel")
|
||||
settingModel := new(models.Setting)
|
||||
if settingModel.IsChannelExist(channel) {
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.CommonFailure("Channel已存在")
|
||||
}
|
||||
_, err := settingModel.CreateChannel(channel)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func RemoveSlackChannel(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveChannel(id)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region 邮件
|
||||
func Mail(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
jsonResp := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return jsonResp.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
return jsonResp.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)"`
|
||||
}
|
||||
|
||||
func UpdateMail(ctx *macaron.Context, form MailServerForm) string {
|
||||
jsonByte, _ := json.Marshal(form)
|
||||
settingModel := new(models.Setting)
|
||||
|
||||
template := ctx.QueryTrim("template")
|
||||
err := settingModel.UpdateMail(string(jsonByte), template)
|
||||
|
||||
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 == "" {
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.CommonFailure("用户名、邮箱均不能为空")
|
||||
}
|
||||
_, err := settingModel.CreateMailUser(username, email)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func RemoveMailUser(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveMailUser(id)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func WebHook(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
webHook, err := settingModel.Webhook()
|
||||
jsonResp := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return jsonResp.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
return jsonResp.Success("", webHook)
|
||||
}
|
||||
|
||||
func UpdateWebHook(ctx *macaron.Context) string {
|
||||
url := ctx.QueryTrim("url")
|
||||
template := ctx.QueryTrim("template")
|
||||
settingModel := new(models.Setting)
|
||||
err := settingModel.UpdateWebHook(url, template)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,316 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/go-macaron/gzip"
|
||||
"github.com/go-macaron/toolbox"
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/routers/host"
|
||||
"github.com/ouqiang/gocron/internal/routers/install"
|
||||
"github.com/ouqiang/gocron/internal/routers/loginlog"
|
||||
"github.com/ouqiang/gocron/internal/routers/manage"
|
||||
"github.com/ouqiang/gocron/internal/routers/task"
|
||||
"github.com/ouqiang/gocron/internal/routers/tasklog"
|
||||
"github.com/ouqiang/gocron/internal/routers/user"
|
||||
"github.com/rakyll/statik/fs"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
_ "github.com/ouqiang/gocron/internal/statik"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPrefix = "/api"
|
||||
staticDir = "public"
|
||||
)
|
||||
|
||||
var statikFS http.FileSystem
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
statikFS, err = fs.New()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register 路由注册
|
||||
func Register(m *macaron.Macaron) {
|
||||
m.SetURLPrefix(urlPrefix)
|
||||
// 所有GET方法,自动注册HEAD方法
|
||||
m.SetAutoHead(true)
|
||||
m.Get("/", func(ctx *macaron.Context) {
|
||||
file, err := statikFS.Open("/index.html")
|
||||
if err != nil {
|
||||
logger.Error("读取首页文件失败: %s", err)
|
||||
ctx.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
io.Copy(ctx.Resp, file)
|
||||
|
||||
})
|
||||
// 系统安装
|
||||
m.Group("/install", func() {
|
||||
m.Post("/store", binding.Bind(install.InstallForm{}), install.Store)
|
||||
m.Get("/status", func(ctx *macaron.Context) string {
|
||||
jsonResp := utils.JsonResponse{}
|
||||
return jsonResp.Success("", app.Installed)
|
||||
})
|
||||
})
|
||||
|
||||
// 用户
|
||||
m.Group("/user", func() {
|
||||
m.Get("", user.Index)
|
||||
m.Get("/:id", user.Detail)
|
||||
m.Post("/store", binding.Bind(user.UserForm{}), user.Store)
|
||||
m.Post("/remove/:id", user.Remove)
|
||||
m.Post("/login", user.ValidateLogin)
|
||||
m.Post("/enable/:id", user.Enable)
|
||||
m.Post("/disable/:id", user.Disable)
|
||||
m.Post("/editMyPassword", user.UpdateMyPassword)
|
||||
m.Post("/editPassword/:id", user.UpdatePassword)
|
||||
})
|
||||
|
||||
// 定时任务
|
||||
m.Group("/task", func() {
|
||||
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
|
||||
m.Get("/:id", task.Detail)
|
||||
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("/:id", host.Detail)
|
||||
m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
|
||||
m.Get("", host.Index)
|
||||
m.Get("/all", host.All)
|
||||
m.Get("/ping/:id", host.Ping)
|
||||
m.Post("/remove/:id", host.Remove)
|
||||
})
|
||||
|
||||
// 管理
|
||||
m.Group("/system", func() {
|
||||
m.Group("/slack", func() {
|
||||
m.Get("", manage.Slack)
|
||||
m.Post("/update", manage.UpdateSlack)
|
||||
m.Post("/channel", manage.CreateSlackChannel)
|
||||
m.Post("/channel/remove/:id", manage.RemoveSlackChannel)
|
||||
})
|
||||
m.Group("/mail", func() {
|
||||
m.Get("", manage.Mail)
|
||||
m.Post("/update", binding.Bind(manage.MailServerForm{}), manage.UpdateMail)
|
||||
m.Post("/user", manage.CreateMailUser)
|
||||
m.Post("/user/remove/:id", manage.RemoveMailUser)
|
||||
})
|
||||
m.Group("/webhook", func() {
|
||||
m.Get("", manage.WebHook)
|
||||
m.Post("/update", manage.UpdateWebHook)
|
||||
})
|
||||
m.Get("/login-log", loginlog.Index)
|
||||
})
|
||||
|
||||
// API
|
||||
m.Group("/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) string {
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Failure(utils.NotFound, "您访问的页面不存在")
|
||||
})
|
||||
// 50x错误
|
||||
m.InternalServerError(func(ctx *macaron.Context) string {
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.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(
|
||||
"",
|
||||
macaron.StaticOptions{
|
||||
Prefix: staticDir,
|
||||
FileSystem: statikFS,
|
||||
},
|
||||
),
|
||||
)
|
||||
if macaron.Env == macaron.DEV {
|
||||
m.Use(toolbox.Toolboxer(m))
|
||||
}
|
||||
m.Use(macaron.Renderer())
|
||||
m.Use(checkAppInstall)
|
||||
m.Use(ipAuth)
|
||||
m.Use(userAuth)
|
||||
m.Use(urlAuth)
|
||||
}
|
||||
|
||||
// region 自定义中间件
|
||||
|
||||
/** 检测应用是否已安装 **/
|
||||
func checkAppInstall(ctx *macaron.Context) {
|
||||
if app.Installed {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(ctx.Req.URL.Path, "/install") || ctx.Req.URL.Path == "/" {
|
||||
return
|
||||
}
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
data := jsonResp.Failure(utils.AppNotInstall, "应用未安装")
|
||||
ctx.Write([]byte(data))
|
||||
}
|
||||
|
||||
// IP验证, 通过反向代理访问gocron,需设置Header X-Real-IP才能获取到客户端真实IP
|
||||
func ipAuth(ctx *macaron.Context) {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
allowIpsStr := app.Setting.AllowIps
|
||||
if allowIpsStr == "" {
|
||||
return
|
||||
}
|
||||
clientIp := ctx.RemoteAddr()
|
||||
allowIps := strings.Split(allowIpsStr, ",")
|
||||
if utils.InStringSlice(allowIps, clientIp) {
|
||||
return
|
||||
}
|
||||
logger.Warnf("非法IP访问-%s", clientIp)
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
data := jsonResp.Failure(utils.UnauthorizedError, "您无权限访问")
|
||||
|
||||
ctx.Write([]byte(data))
|
||||
}
|
||||
|
||||
// 用户认证
|
||||
func userAuth(ctx *macaron.Context) {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
user.RestoreToken(ctx)
|
||||
if user.IsLogin(ctx) {
|
||||
return
|
||||
}
|
||||
uri := strings.TrimRight(ctx.Req.URL.Path, "/")
|
||||
if strings.HasPrefix(uri, "/v1") {
|
||||
return
|
||||
}
|
||||
excludePaths := []string{"", "/user/login", "/install/status"}
|
||||
for _, path := range excludePaths {
|
||||
if uri == path {
|
||||
return
|
||||
}
|
||||
}
|
||||
jsonResp := utils.JsonResponse{}
|
||||
data := jsonResp.Failure(utils.AuthError, "认证失败")
|
||||
ctx.Write([]byte(data))
|
||||
|
||||
}
|
||||
|
||||
// URL权限验证
|
||||
func urlAuth(ctx *macaron.Context) {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
if user.IsAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
uri := strings.TrimRight(ctx.Req.URL.Path, "/")
|
||||
if strings.HasPrefix(uri, "/v1") {
|
||||
return
|
||||
}
|
||||
// 普通用户允许访问的URL地址
|
||||
allowPaths := []string{
|
||||
"",
|
||||
"/install/status",
|
||||
"/task",
|
||||
"/task/log",
|
||||
"/host",
|
||||
"/host/all",
|
||||
"/user/login",
|
||||
"/user/editMyPassword",
|
||||
}
|
||||
for _, path := range allowPaths {
|
||||
if path == uri {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
data := jsonResp.Failure(utils.UnauthorizedError, "您无权限访问")
|
||||
ctx.Write([]byte(data))
|
||||
}
|
||||
|
||||
/** API接口签名验证 **/
|
||||
func apiAuth(ctx *macaron.Context) {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
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
|
||||
@@ -0,0 +1,297 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ouqiang/goutil"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/jakecoffman/cron"
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/routers/base"
|
||||
"github.com/ouqiang/gocron/internal/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)"`
|
||||
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,4)"`
|
||||
NotifyType int8 `binding:"In(1,2,3,4)"`
|
||||
NotifyReceiverId string
|
||||
NotifyKeyword string
|
||||
}
|
||||
|
||||
func (f TaskForm) Error(ctx *macaron.Context, errs binding.Errors) {
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
|
||||
ctx.Resp.Write([]byte(content))
|
||||
}
|
||||
|
||||
// 首页
|
||||
func Index(ctx *macaron.Context) string {
|
||||
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)
|
||||
}
|
||||
for i, item := range tasks {
|
||||
tasks[i].NextRunTime = service.ServiceTask.NextRunTime(item)
|
||||
}
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, map[string]interface{}{
|
||||
"total": total,
|
||||
"data": tasks,
|
||||
})
|
||||
}
|
||||
|
||||
// Detail 任务详情
|
||||
func Detail(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
jsonResp := utils.JsonResponse{}
|
||||
if err != nil || task.Id == 0 {
|
||||
logger.Errorf("编辑任务#获取任务详情失败#任务ID-%d", id)
|
||||
return jsonResp.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, task)
|
||||
}
|
||||
|
||||
// 保存任务 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("请选择主机名")
|
||||
}
|
||||
|
||||
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.NotifyKeyword = form.NotifyKeyword
|
||||
taskModel.Spec = form.Spec
|
||||
taskModel.Level = form.Level
|
||||
taskModel.DependencyStatus = form.DependencyStatus
|
||||
taskModel.DependencyTaskId = strings.TrimSpace(form.DependencyTaskId)
|
||||
if taskModel.NotifyStatus > 0 && taskModel.NotifyType != 3 && 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.RetryInterval > 3600 || taskModel.RetryInterval < 0 {
|
||||
return json.CommonFailure("任务重试间隔时间取值0-3600")
|
||||
}
|
||||
|
||||
if taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
|
||||
taskModel.DependencyStatus != models.TaskDependencyStatusWeak {
|
||||
return json.CommonFailure("请选择依赖关系")
|
||||
}
|
||||
|
||||
if taskModel.Level == models.TaskLevelParent {
|
||||
err = goutil.PanicToError(func() {
|
||||
cron.Parse(form.Spec)
|
||||
})
|
||||
if err != nil {
|
||||
return json.CommonFailure("crontab表达式解析失败", err)
|
||||
}
|
||||
} else {
|
||||
taskModel.DependencyTaskId = ""
|
||||
taskModel.Spec = ""
|
||||
}
|
||||
|
||||
if id > 0 && taskModel.DependencyTaskId != "" {
|
||||
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
|
||||
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
|
||||
return json.CommonFailure("不允许设置当前任务为子任务")
|
||||
}
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
// 任务添加后开始调度执行
|
||||
taskModel.Status = models.Running
|
||||
id, err = taskModel.Create()
|
||||
} else {
|
||||
_, err = taskModel.UpdateBean(id)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return json.CommonFailure("保存失败", err)
|
||||
}
|
||||
|
||||
taskHostModel := new(models.TaskHost)
|
||||
if form.Protocol == models.TaskRPC {
|
||||
hostIdStrList := strings.Split(form.HostId, ",")
|
||||
hostIds := make([]int, len(hostIdStrList))
|
||||
for i, hostIdStr := range hostIdStrList {
|
||||
hostIds[i], _ = strconv.Atoi(hostIdStr)
|
||||
}
|
||||
taskHostModel.Add(id, hostIds)
|
||||
} else {
|
||||
taskHostModel.Remove(id)
|
||||
}
|
||||
|
||||
status, _ := taskModel.GetStatus(id)
|
||||
if status == models.Enabled && taskModel.Level == models.TaskLevelParent {
|
||||
addTaskToTimer(id)
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
_, err := taskModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
|
||||
taskHostModel := new(models.TaskHost)
|
||||
taskHostModel.Remove(id)
|
||||
|
||||
service.ServiceTask.Remove(id)
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
task.Spec = "手动运行"
|
||||
service.ServiceTask.Run(task)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if status == models.Enabled {
|
||||
addTaskToTimer(id)
|
||||
} else {
|
||||
service.ServiceTask.Remove(id)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return params
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package tasklog
|
||||
|
||||
// 任务日志
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/routers/base"
|
||||
"github.com/ouqiang/gocron/internal/service"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func Index(ctx *macaron.Context) string {
|
||||
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)
|
||||
}
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, map[string]interface{}{
|
||||
"total": total,
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
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)
|
||||
}
|
||||
|
||||
// 停止运行中的任务
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return params
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/utils"
|
||||
"github.com/ouqiang/gocron/internal/routers/base"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
const tokenDuration = 4 * time.Hour
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Index 用户列表页
|
||||
func Index(ctx *macaron.Context) string {
|
||||
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)
|
||||
}
|
||||
|
||||
jsonResp := utils.JsonResponse{}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, map[string]interface{}{
|
||||
"total": total,
|
||||
"data": users,
|
||||
})
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
|
||||
params := models.CommonMap{}
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// Detail 用户详情
|
||||
func Detail(ctx *macaron.Context) string {
|
||||
userModel := new(models.User)
|
||||
id := ctx.ParamsInt(":id")
|
||||
err := userModel.Find(id)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
jsonResp := utils.JsonResponse{}
|
||||
if userModel.Id == 0 {
|
||||
return jsonResp.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
return jsonResp.Success(utils.SuccessContent, userModel)
|
||||
}
|
||||
|
||||
// 保存任务
|
||||
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)
|
||||
}
|
||||
|
||||
// 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("请输入密码")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// UpdateMyPassword 更新我的密码
|
||||
func UpdateMyPassword(ctx *macaron.Context) 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(ctx), oldPassword) {
|
||||
return json.CommonFailure("原密码输入错误")
|
||||
}
|
||||
_, err := userModel.UpdatePassword(Uid(ctx), newPassword)
|
||||
if err != nil {
|
||||
return json.CommonFailure("修改失败")
|
||||
}
|
||||
|
||||
return json.Success("修改成功", nil)
|
||||
}
|
||||
|
||||
// ValidateLogin 验证用户登录
|
||||
func ValidateLogin(ctx *macaron.Context) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
password := ctx.QueryTrim("password")
|
||||
json := utils.JsonResponse{}
|
||||
if username == "" || password == "" {
|
||||
return json.CommonFailure("用户名、密码不能为空")
|
||||
}
|
||||
userModel := new(models.User)
|
||||
if !userModel.Match(username, password) {
|
||||
return json.CommonFailure("用户名或密码错误")
|
||||
}
|
||||
loginLogModel := new(models.LoginLog)
|
||||
loginLogModel.Username = userModel.Name
|
||||
loginLogModel.Ip = ctx.RemoteAddr()
|
||||
_, err := loginLogModel.Create()
|
||||
if err != nil {
|
||||
logger.Error("记录用户登录日志失败", err)
|
||||
}
|
||||
|
||||
token, err := generateToken(userModel)
|
||||
if err != nil {
|
||||
logger.Errorf("生成jwt失败: %s", err)
|
||||
return json.Failure(utils.AuthError, "认证失败")
|
||||
}
|
||||
|
||||
return json.Success(utils.SuccessContent, map[string]interface{}{
|
||||
"token": token,
|
||||
"uid": userModel.Id,
|
||||
"username": userModel.Name,
|
||||
"is_admin": userModel.IsAdmin,
|
||||
})
|
||||
}
|
||||
|
||||
// Username 获取session中的用户名
|
||||
func Username(ctx *macaron.Context) string {
|
||||
usernameInterface, ok := ctx.Data["username"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if username, ok := usernameInterface.(string); ok {
|
||||
return username
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Uid 获取session中的Uid
|
||||
func Uid(ctx *macaron.Context) int {
|
||||
uidInterface, ok := ctx.Data["uid"]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
if uid, ok := uidInterface.(int); ok {
|
||||
return uid
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// IsLogin 判断用户是否已登录
|
||||
func IsLogin(ctx *macaron.Context) bool {
|
||||
return Uid(ctx) > 0
|
||||
}
|
||||
|
||||
// IsAdmin 判断当前用户是否是管理员
|
||||
func IsAdmin(ctx *macaron.Context) bool {
|
||||
isAdmin, ok := ctx.Data["is_admin"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if v, ok := isAdmin.(int); ok {
|
||||
return v > 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成jwt
|
||||
func generateToken(user *models.User) (string, error) {
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
claims := make(jwt.MapClaims)
|
||||
claims["exp"] = time.Now().Add(tokenDuration).Unix()
|
||||
claims["uid"] = user.Id
|
||||
claims["iat"] = time.Now().Unix()
|
||||
claims["issuer"] = "gocron"
|
||||
claims["username"] = user.Name
|
||||
claims["is_admin"] = user.IsAdmin
|
||||
token.Claims = claims
|
||||
|
||||
return token.SignedString([]byte(app.Setting.AuthSecret))
|
||||
}
|
||||
|
||||
// 还原jwt
|
||||
func RestoreToken(ctx *macaron.Context) error {
|
||||
authToken := ctx.Req.Header.Get("Auth-Token")
|
||||
if authToken == "" {
|
||||
return nil
|
||||
}
|
||||
token, err := jwt.Parse(authToken, func(*jwt.Token) (interface{}, error) {
|
||||
return []byte(app.Setting.AuthSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return errors.New("invalid claims")
|
||||
}
|
||||
ctx.Data["uid"] = int(claims["uid"].(float64))
|
||||
ctx.Data["username"] = claims["username"]
|
||||
ctx.Data["is_admin"] = int(claims["is_admin"].(float64))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/goutil"
|
||||
|
||||
"github.com/jakecoffman/cron"
|
||||
"github.com/ouqiang/gocron/internal/models"
|
||||
"github.com/ouqiang/gocron/internal/modules/app"
|
||||
"github.com/ouqiang/gocron/internal/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/internal/modules/logger"
|
||||
"github.com/ouqiang/gocron/internal/modules/notify"
|
||||
rpcClient "github.com/ouqiang/gocron/internal/modules/rpc/client"
|
||||
pb "github.com/ouqiang/gocron/internal/modules/rpc/proto"
|
||||
)
|
||||
|
||||
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 {
|
||||
wg sync.WaitGroup
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (tc *TaskCount) Add() {
|
||||
tc.wg.Add(1)
|
||||
}
|
||||
|
||||
func (tc *TaskCount) Done() {
|
||||
tc.wg.Done()
|
||||
}
|
||||
|
||||
func (tc *TaskCount) Exit() {
|
||||
tc.wg.Done()
|
||||
<-tc.exit
|
||||
}
|
||||
|
||||
func (tc *TaskCount) Wait() {
|
||||
tc.Add()
|
||||
tc.wg.Wait()
|
||||
close(tc.exit)
|
||||
}
|
||||
|
||||
// 任务ID作为Key
|
||||
type Instance struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
// 是否有任务处于运行中
|
||||
func (i *Instance) has(key int) bool {
|
||||
_, ok := i.m.Load(key)
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (i *Instance) add(key int) {
|
||||
i.m.Store(key, struct{}{})
|
||||
}
|
||||
|
||||
func (i *Instance) done(key int) {
|
||||
i.m.Delete(key)
|
||||
}
|
||||
|
||||
type Task struct{}
|
||||
|
||||
type TaskResult struct {
|
||||
Result string
|
||||
Err error
|
||||
RetryTimes int8
|
||||
}
|
||||
|
||||
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
|
||||
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()
|
||||
|
||||
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("定时任务初始化#获取任务列表错误: %s", err)
|
||||
}
|
||||
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.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
|
||||
}
|
||||
|
||||
cronName := strconv.Itoa(taskModel.Id)
|
||||
err := goutil.PanicToError(func() {
|
||||
serviceCron.AddFunc(taskModel.Spec, taskFunc, cronName)
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("添加任务到调度器失败#", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (task Task) NextRunTime(taskModel models.Task) time.Time {
|
||||
if taskModel.Level != models.TaskLevelParent ||
|
||||
taskModel.Status != models.Enabled {
|
||||
return time.Time{}
|
||||
}
|
||||
entries := serviceCron.Entries()
|
||||
taskName := strconv.Itoa(taskModel.Id)
|
||||
for _, item := range entries {
|
||||
if item.Name == taskName {
|
||||
return item.Next
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// 停止运行中的任务
|
||||
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)()
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
Run(taskModel models.Task, taskUniqueId int64) (string, error)
|
||||
}
|
||||
|
||||
// HTTP任务
|
||||
type HTTPHandler struct{}
|
||||
|
||||
// http任务执行时间不超过300秒
|
||||
const HttpExecTimeout = 300
|
||||
|
||||
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, fmt.Errorf("HTTP状态码非200-->%d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp.Body, err
|
||||
}
|
||||
|
||||
// RPC调用执行任务
|
||||
type RPCHandler struct{}
|
||||
|
||||
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:%d]\n%s\n%s\n\n",
|
||||
th.Alias, th.Name, th.Port, errorMessage, output,
|
||||
)
|
||||
resultChan <- TaskResult{Err: err, Result: outputMessage}
|
||||
}(taskHost)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 创建任务日志
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 更新任务日志
|
||||
func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) {
|
||||
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 {
|
||||
handler := createHandler(taskModel)
|
||||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
taskFunc := func() {
|
||||
taskCount.Add()
|
||||
defer taskCount.Done()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// 任务前置操作
|
||||
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)
|
||||
|
||||
return taskLogId
|
||||
}
|
||||
|
||||
// 任务执行后置操作
|
||||
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)
|
||||
}
|
||||
|
||||
// 执行依赖任务, 多个任务并发执行
|
||||
func execDependencyTask(taskModel models.Task, taskResult TaskResult) {
|
||||
// 父任务才能执行子任务
|
||||
if taskModel.Level != models.TaskLevelParent {
|
||||
return
|
||||
}
|
||||
|
||||
// 是否存在子任务
|
||||
dependencyTaskId := strings.TrimSpace(taskModel.DependencyTaskId)
|
||||
if dependencyTaskId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 父子任务关系为强依赖, 父任务执行失败, 不执行依赖任务
|
||||
if taskModel.DependencyStatus == models.TaskDependencyStatusStrong && taskResult.Err != nil {
|
||||
logger.Infof("父子任务为强依赖关系, 父任务执行失败, 不运行依赖任务#主任务ID-%d", taskModel.Id)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取子任务
|
||||
model := new(models.Task)
|
||||
tasks, err := model.GetDependencyTaskList(dependencyTaskId)
|
||||
if err != nil {
|
||||
logger.Errorf("获取依赖任务失败#主任务ID-%d#%s", taskModel.Id, err.Error())
|
||||
return
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
logger.Errorf("依赖任务列表为空#主任务ID-%d", taskModel.Id)
|
||||
}
|
||||
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 == 3 {
|
||||
// 关键字匹配通知
|
||||
if !strings.Contains(taskResult.Result, taskModel.NotifyKeyword) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if taskModel.NotifyStatus == 1 && taskResult.Err == nil {
|
||||
// 执行失败才发送通知
|
||||
return
|
||||
}
|
||||
if taskModel.NotifyType != 3 && 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,
|
||||
"task_id": taskModel.Id,
|
||||
"remark": taskModel.Remark,
|
||||
}
|
||||
notify.Push(msg)
|
||||
}
|
||||
|
||||
// 执行具体任务
|
||||
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}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
FROM alpine:3.7
|
||||
|
||||
ENV GOCRON_AGENT_VERSION=v1.5
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata bash \
|
||||
&& mkdir -p /app \
|
||||
&& wget -P /tmp https://github.com/ouqiang/gocron/releases/download/${GOCRON_AGENT_VERSION}/gocron-node-${GOCRON_AGENT_VERSION}-linux-amd64.tar.gz \
|
||||
&& cd /tmp \
|
||||
&& tar zvxf gocron-node-${GOCRON_AGENT_VERSION}-linux-amd64.tar.gz \
|
||||
&& mv /tmp/gocron-node-linux-amd64/gocron-node /app \
|
||||
&& rm -rf /tmp/* \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 5921
|
||||
|
||||
ENTRYPOINT ["/app/gocron-node", "-allow-root"]
|
||||
@@ -0,0 +1,38 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: gocron-agent
|
||||
namespace: kube-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
daemon: gocron-agent
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
daemon: gocron-agent
|
||||
name: gocron-agent
|
||||
spec:
|
||||
containers:
|
||||
- name: gocron-agent
|
||||
image: crontab-agent:0.5
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5921
|
||||
hostPort: 5921
|
||||
name: gocron-agent
|
||||
protocol: TCP
|
||||
resources: {}
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
volumeMounts:
|
||||
- mountPath: /var/log
|
||||
name: syslog
|
||||
hostNetwork: true
|
||||
hostPID: true
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- hostPath:
|
||||
path: /var/log
|
||||
type: Directory
|
||||
name: syslog
|
||||
@@ -0,0 +1,51 @@
|
||||
# 改为用StatefulSet部署,把配置目录挂载到外置存储,以防container down后重启
|
||||
# 如果使用历史数据库,需要先配置安装,然后在conf目录下创建install.lock文件,最后重启进程即可
|
||||
#
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gocron
|
||||
labels:
|
||||
app: gocron
|
||||
spec:
|
||||
ports:
|
||||
- port: 5920
|
||||
name: gocron
|
||||
type: NodePort
|
||||
selector:
|
||||
app: gocron
|
||||
---
|
||||
apiVersion: apps/v1beta1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: gocron
|
||||
spec:
|
||||
serviceName: "gocron"
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gocron
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: gocron
|
||||
image: ouqg/gocron:latest
|
||||
ports:
|
||||
- containerPort: 5920
|
||||
name: gocron
|
||||
volumeMounts:
|
||||
- name: gocron-vol
|
||||
mountPath: /app/conf
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: gocron-vol
|
||||
spec:
|
||||
storageClassName: rbd
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 100Mi
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mysql
|
||||
#namespace: storage
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
ports:
|
||||
- port: 3306
|
||||
name: mysql
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: mysql
|
||||
---
|
||||
apiVersion: apps/v1beta1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: mysql
|
||||
#namespace: storage
|
||||
spec:
|
||||
serviceName: "mysql"
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: mysql
|
||||
image: mysql:5.6
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
value: password
|
||||
- name: MYSQL_DATABASE
|
||||
value: crontab
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
name: mysql
|
||||
volumeMounts:
|
||||
- name: mysql-vol
|
||||
mountPath: /var/lib/mysql
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: mysql-vol
|
||||
spec:
|
||||
storageClassName: rbd # 需要storageClassName
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 100Gi
|
||||
@@ -0,0 +1,72 @@
|
||||
GO111MODULE=on
|
||||
|
||||
.PHONY: build
|
||||
build: gocron node
|
||||
|
||||
.PHONY: build-race
|
||||
build-race: enable-race build
|
||||
|
||||
.PHONY: run
|
||||
run: build kill
|
||||
./bin/gocron-node &
|
||||
./bin/gocron web -e dev
|
||||
|
||||
.PHONY: run-race
|
||||
run-race: enable-race run
|
||||
|
||||
.PHONY: kill
|
||||
kill:
|
||||
-killall gocron-node
|
||||
|
||||
.PHONY: gocron
|
||||
gocron:
|
||||
go build $(RACE) -o bin/gocron ./cmd/gocron
|
||||
|
||||
.PHONY: node
|
||||
node:
|
||||
go build $(RACE) -o bin/gocron-node ./cmd/node
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test $(RACE) ./...
|
||||
|
||||
.PHONY: test-race
|
||||
test-race: enable-race test
|
||||
|
||||
.PHONY: enable-race
|
||||
enable-race:
|
||||
$(eval RACE = -race)
|
||||
|
||||
.PHONY: package
|
||||
package: build-vue statik
|
||||
bash ./package.sh
|
||||
|
||||
.PHONY: package-all
|
||||
package-all: build-vue statik
|
||||
bash ./package.sh -p 'linux darwin windows'
|
||||
|
||||
.PHONY: build-vue
|
||||
build-vue:
|
||||
cd web/vue && yarn run build
|
||||
cp -r web/vue/dist/* web/public/
|
||||
|
||||
.PHONY: install-vue
|
||||
install-vue:
|
||||
cd web/vue && yarn install
|
||||
|
||||
.PHONY: run-vue
|
||||
run-vue:
|
||||
cd web/vue && yarn run dev
|
||||
|
||||
.PHONY: statik
|
||||
statik:
|
||||
go get github.com/rakyll/statik
|
||||
go generate ./...
|
||||
|
||||
.PHONY: lint
|
||||
golangci-lint run
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm bin/gocron
|
||||
rm bin/gocron-node
|
||||
@@ -1,85 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
|
||||
// 延迟任务
|
||||
type DelayTask struct {
|
||||
Id int64 `xorm:"bigint pk autoincr"`
|
||||
Url string `xorm:"varchar(128) not null"`
|
||||
Params string `xorm:"varchar(256) not null default '' "`
|
||||
Delay int `xorm:"mediumint notnull default 0"` // 延迟时间
|
||||
Status Status `xorm:"tinyint notnull index(u_status_created) default 5"` // 状态 0:执行失败 1:执行中 2:执行成功 5: 待执行
|
||||
Created time.Time `xorm:"datetime notnull created index(u_status_created)"`
|
||||
Updated time.Time `xorm:"datetime updated"`
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
func (task *DelayTask) Create() (insertId int64, err error) {
|
||||
_, err = Db.Insert(task)
|
||||
if err == nil {
|
||||
insertId = task.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有待执行任务
|
||||
func (task *DelayTask) ActiveList(endTime time.Time) ([]DelayTask, error) {
|
||||
list := make([]DelayTask, 0)
|
||||
fields := "id,url,params,delay,created"
|
||||
err := Db.Where("status IN (?, ?) AND created <= ?", Waiting, Running, endTime.Format(DefaultTimeFormat)).Cols(fields).Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 获取待执行任务数量
|
||||
func (task *DelayTask) ActiveNum(endTime time.Time) (int, error) {
|
||||
count ,err := Db.Where("status IN (?, ?) AND created <= ?", Waiting, Running, endTime.Format(DefaultTimeFormat)).Count(task)
|
||||
|
||||
return int(count), err
|
||||
}
|
||||
|
||||
func (task *DelayTask) List(params CommonMap) ([]DelayTask, error) {
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]DelayTask, 0)
|
||||
session := Db.Desc("id")
|
||||
task.parseWhere(session, params)
|
||||
err := session.Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
|
||||
// 更新任务状态
|
||||
func (task *DelayTask) UpdateStatus(id int64, status Status) (int64, error) {
|
||||
return Db.Table(task).Id(id).Update(CommonMap{
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (task *DelayTask) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func (task *DelayTask) Remove(id int) (int64, error) {
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("created <= ?", t.Format(DefaultTimeFormat)).Delete(task)
|
||||
}
|
||||
|
||||
func (task *DelayTask) Total(params CommonMap) (int64, error) {
|
||||
session := Db.NewSession()
|
||||
task.parseWhere(session, params)
|
||||
return session.Count(task)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"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:"-"`
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (host *Host) Create() (insertId int16, err error) {
|
||||
_, err = Db.Insert(host)
|
||||
if err == nil {
|
||||
insertId = host.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (host *Host) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(new(Host))
|
||||
}
|
||||
|
||||
func (host *Host) Find(id int) error {
|
||||
_, err := Db.Id(id).Get(host)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (host *Host) AllList() ([]Host, error) {
|
||||
list := make([]Host, 0)
|
||||
err := Db.Cols("name,port").Desc("id").Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (host *Host) Total(params CommonMap) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"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:"-"`
|
||||
}
|
||||
|
||||
func (log *LoginLog) Create() (insertId int, err error) {
|
||||
_, err = Db.Insert(log)
|
||||
if err == nil {
|
||||
insertId = log.Id
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (log *LoginLog) Total() (int64, error) {
|
||||
return Db.Count(log)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// 创建数据库表
|
||||
|
||||
type Migration struct{}
|
||||
|
||||
func (migration *Migration) Exec(dbName string) error {
|
||||
if !isDatabaseExist(dbName) {
|
||||
return errors.New("数据库不存在")
|
||||
}
|
||||
setting := new(Setting)
|
||||
task := new(Task)
|
||||
tables := []interface{}{
|
||||
&User{}, task, &DelayTask{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
|
||||
}
|
||||
for _, table := range tables {
|
||||
exist, err:= Db.IsTableExist(table)
|
||||
if exist {
|
||||
return errors.New("数据表已存在")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = Db.Sync2(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setting.InitBasicField()
|
||||
task.CreateTestTask()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建数据库
|
||||
func isDatabaseExist(name string) bool {
|
||||
_, err := Db.Exec("use ?", name)
|
||||
|
||||
return err != nil
|
||||
}
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
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"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Status int8
|
||||
type CommonMap map[string]interface{}
|
||||
|
||||
var TablePrefix string = ""
|
||||
var Db *xorm.Engine
|
||||
|
||||
const (
|
||||
Disabled Status = 0 // 禁用
|
||||
Failure Status = 0 // 失败
|
||||
Enabled Status = 1 // 启用
|
||||
Running Status = 1 // 运行中
|
||||
Finish Status = 2 // 完成
|
||||
Cancel Status = 3 // 取消
|
||||
Waiting Status = 5 // 等待中
|
||||
)
|
||||
|
||||
const (
|
||||
Page = 1 // 当前页数
|
||||
PageSize = 20 // 每页多少条数据
|
||||
MaxPageSize = 100000 // 每次最多取多少条
|
||||
)
|
||||
|
||||
const DefaultTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
type BaseModel struct {
|
||||
Page int `xorm:"-"`
|
||||
PageSize int `xorm:"-"`
|
||||
}
|
||||
|
||||
func (model *BaseModel) parsePageAndPageSize(params CommonMap) {
|
||||
page, ok := params["Page"]
|
||||
if ok {
|
||||
model.Page = page.(int)
|
||||
}
|
||||
pageSize, ok := params["PageSize"]
|
||||
if ok {
|
||||
model.PageSize = pageSize.(int)
|
||||
}
|
||||
if model.Page <= 0 {
|
||||
model.Page = Page
|
||||
}
|
||||
if model.PageSize <= 0 || model.PageSize > MaxPageSize {
|
||||
model.PageSize = PageSize
|
||||
}
|
||||
}
|
||||
|
||||
func (model *BaseModel) pageLimitOffset() int {
|
||||
return (model.Page - 1) * model.PageSize
|
||||
}
|
||||
|
||||
// 创建Db
|
||||
func CreateDb() *xorm.Engine {
|
||||
config := getDbConfig()
|
||||
dsn := getDbEngineDSN(config["engine"], config)
|
||||
engine, err := xorm.NewEngine(config["engine"], dsn)
|
||||
if err != nil {
|
||||
logger.Fatal("创建xorm引擎失败", err)
|
||||
}
|
||||
maxIdleConns, err := strconv.Atoi(config["max_idle_conns"])
|
||||
maxOpenConns, err := strconv.Atoi(config["max_open_conns"])
|
||||
if maxIdleConns <= 0 {
|
||||
maxIdleConns = 30
|
||||
}
|
||||
if maxOpenConns <= 0 {
|
||||
maxOpenConns = 100
|
||||
}
|
||||
engine.SetMaxIdleConns(maxIdleConns)
|
||||
engine.SetMaxOpenConns(maxOpenConns)
|
||||
|
||||
if config["prefix"] != "" {
|
||||
// 设置表前缀
|
||||
TablePrefix = config["prefix"]
|
||||
mapper := core.NewPrefixMapper(core.SnakeMapper{}, config["prefix"])
|
||||
engine.SetTableMapper(mapper)
|
||||
}
|
||||
// 本地环境开启日志
|
||||
if macaron.Env == macaron.DEV {
|
||||
engine.ShowSQL(true)
|
||||
engine.Logger().SetLevel(core.LOG_DEBUG)
|
||||
}
|
||||
|
||||
|
||||
return engine
|
||||
}
|
||||
|
||||
// 创建临时数据库连接
|
||||
func CreateTmpDb(config map[string]string) (*xorm.Engine, error) {
|
||||
dsn := getDbEngineDSN(config["engine"], config)
|
||||
|
||||
return xorm.NewEngine(config["engine"], dsn)
|
||||
}
|
||||
|
||||
// 获取数据库引擎DSN mysql,sqlite
|
||||
func getDbEngineDSN(engine string, config map[string]string) string {
|
||||
engine = strings.ToLower(engine)
|
||||
var dsn string = ""
|
||||
switch engine {
|
||||
case "mysql":
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s",
|
||||
config["user"],
|
||||
config["password"],
|
||||
config["host"],
|
||||
config["port"],
|
||||
config["database"],
|
||||
config["charset"])
|
||||
}
|
||||
|
||||
return dsn
|
||||
}
|
||||
|
||||
|
||||
// 获取数据库配置
|
||||
func getDbConfig() map[string]string {
|
||||
var db map[string]string = make(map[string]string)
|
||||
db["user"] = app.Setting.Key("db.user").String()
|
||||
db["password"] = app.Setting.Key("db.password").String()
|
||||
db["host"] = app.Setting.Key("db.host").String()
|
||||
db["port"] = app.Setting.Key("db.port").String()
|
||||
db["database"] = app.Setting.Key("db.database").String()
|
||||
db["charset"] = app.Setting.Key("db.charset").String()
|
||||
db["prefix"] = app.Setting.Key("db.prefix").String()
|
||||
db["engine"] = app.Setting.Key("db.engine").String()
|
||||
db["max_idle_conns"] = app.Setting.Key("db.max.idle.conns").String()
|
||||
db["max_open_conns"] = app.Setting.Key("db.max.open.conns").String()
|
||||
|
||||
return db
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"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 '' "`
|
||||
}
|
||||
|
||||
const SlackCode = "slack"
|
||||
const SlackUrlKey = "url"
|
||||
const SlackChannelKey = "channel"
|
||||
|
||||
const MailCode = "mail"
|
||||
const MailServerKey = "server"
|
||||
const MailUserKey = "user"
|
||||
|
||||
// 初始化基本字段 邮件、slack等
|
||||
func (setting *Setting) InitBasicField() {
|
||||
setting.Code = SlackCode;
|
||||
setting.Key = SlackUrlKey
|
||||
Db.Insert(setting)
|
||||
|
||||
setting.Id = 0
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailServerKey
|
||||
Db.Insert(setting)
|
||||
}
|
||||
|
||||
// region slack配置
|
||||
|
||||
type Slack struct {
|
||||
Url string
|
||||
Channels []Channel
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
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
|
||||
}
|
||||
|
||||
setting.formatSlack(list, &slack)
|
||||
|
||||
return slack, err
|
||||
}
|
||||
|
||||
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 webhook url
|
||||
func (setting *Setting) UpdateSlackUrl(url string) (int64, error) {
|
||||
setting.Value = url
|
||||
|
||||
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
|
||||
|
||||
return Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) IsChannelExist(channel string) (bool) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
count, _ := Db.Count(setting)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
type Mail struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
MailUsers []MailUser
|
||||
}
|
||||
|
||||
type MailUser struct {
|
||||
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
|
||||
}
|
||||
|
||||
setting.formatMail(list, &mail)
|
||||
|
||||
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) 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)
|
||||
|
||||
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)
|
||||
}
|
||||
// endregion
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/go-xorm/xorm"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type TaskProtocol int8
|
||||
|
||||
const (
|
||||
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
|
||||
TaskRPC // RPC方式执行命令
|
||||
)
|
||||
|
||||
// 任务
|
||||
type Task struct {
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
Name string `xorm:"varchar(32) notnull"` // 任务名称
|
||||
Spec string `xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `xorm:"tinyint notnull"` // 协议 1:http 2:系统命令
|
||||
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
|
||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
|
||||
HostId int16 `xorm:"smallint notnull default 0"` // RPC host id,
|
||||
NotifyStatus int8 `xorm:"smallint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
|
||||
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
|
||||
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
||||
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
|
||||
Status Status `xorm:"tinyint notnull default 0"` // 状态 1:正常 0:停止
|
||||
Created time.Time `xorm:"datetime notnull created"` // 创建时间
|
||||
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
type TaskHost struct {
|
||||
Task `xorm:"extends"`
|
||||
Name string
|
||||
Port int
|
||||
Alias string
|
||||
}
|
||||
|
||||
func (TaskHost) TableName() string {
|
||||
return TablePrefix + "task"
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (task *Task) Create() (insertId int, err error) {
|
||||
_, err = Db.Insert(task)
|
||||
if err == nil {
|
||||
insertId = task.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 新增测试任务
|
||||
func (task *Task) CreateTestTask() {
|
||||
// HTTP任务
|
||||
task.Name = "测试HTTP任务"
|
||||
task.Protocol = TaskHTTP
|
||||
task.Spec = "*/30 * * * * *"
|
||||
// 查询IP地址区域信息
|
||||
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
|
||||
task.Status = Enabled
|
||||
task.Create()
|
||||
}
|
||||
|
||||
func (task *Task) UpdateBean(id int) (int64, error) {
|
||||
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,notify_status,notify_type,notify_receiver_id").Update(task)
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (task *Task) Update(id int, data CommonMap) (int64, error) {
|
||||
return Db.Table(task).ID(id).Update(data)
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (task *Task) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(task)
|
||||
}
|
||||
|
||||
// 禁用
|
||||
func (task *Task) Disable(id int) (int64, error) {
|
||||
return task.Update(id, CommonMap{"status": Disabled})
|
||||
}
|
||||
|
||||
// 激活
|
||||
func (task *Task) Enable(id int) (int64, error) {
|
||||
return task.Update(id, CommonMap{"status": Enabled})
|
||||
}
|
||||
|
||||
// 获取所有激活任务
|
||||
func (task *Task) ActiveList() ([]TaskHost, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.status = ?", Enabled).Cols(fields).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 获取某个主机下的所有激活任务
|
||||
func (task *Task) ActiveListByHostId(hostId int16) ([]TaskHost, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.status = ? AND t.host_id = ?", Enabled, hostId).Cols(fields).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 判断主机id是否有引用
|
||||
func (task *Task) HostIdExist(hostId int16) (bool, error) {
|
||||
count, err := Db.Where("host_id = ?", hostId).Count(task);
|
||||
|
||||
return count > 0, 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);
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return task.Status, nil
|
||||
}
|
||||
|
||||
func(task *Task) Detail(id int) (TaskHost, error) {
|
||||
taskHost := TaskHost{}
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
_, err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.id=?", id).Cols(fields).Get(&taskHost)
|
||||
|
||||
return taskHost, err
|
||||
}
|
||||
|
||||
func (task *Task) List(params CommonMap) ([]TaskHost, error) {
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]TaskHost, 0)
|
||||
fields := "t.*, host.alias,host.name"
|
||||
session := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id")
|
||||
task.parseWhere(session, params)
|
||||
err := session.Cols(fields).Desc("t.id").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (task *Task) Total(params CommonMap) (int64, error) {
|
||||
session := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id")
|
||||
task.parseWhere(session, params)
|
||||
return session.Count(task)
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
id, ok := params["Id"]
|
||||
if ok && id.(int) > 0 {
|
||||
session.And("t.id = ?", id)
|
||||
}
|
||||
hostId, ok := params["HostId"]
|
||||
if ok && hostId.(int) > 0 {
|
||||
session.And("host_id = ?", hostId)
|
||||
}
|
||||
name, ok := params["Name"]
|
||||
if ok && name.(string) != "" {
|
||||
session.And("t.name LIKE ?", "%" + name.(string) + "%")
|
||||
}
|
||||
protocol, ok := params["Protocol"]
|
||||
if ok && protocol.(int) > 0 {
|
||||
session.And("protocol = ?", protocol)
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
func hostTableName() []string {
|
||||
return []string{TablePrefix + "host", "host"}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"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:"-"`
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Create() (insertId int64, err error) {
|
||||
_, err = Db.Insert(taskLog)
|
||||
if err == nil {
|
||||
insertId = taskLog.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (taskLog *TaskLog) Update(id int64, data CommonMap) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 清空表
|
||||
func (taskLog *TaskLog) Clear() (int64, error) {
|
||||
return Db.Where("1=1").Delete(taskLog);
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func (taskLog *TaskLog) Remove(id int) (int64, error) {
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Total(params CommonMap) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
const PasswordSaltLength = 6
|
||||
|
||||
// 用户model
|
||||
type User struct {
|
||||
Id int `xorm:"pk autoincr notnull "`
|
||||
Name string `xorm:"varchar(32) notnull unique"` // 用户名
|
||||
Password string `xorm:"char(32) notnull "` // 密码
|
||||
Salt string `xorm:"char(6) notnull "` // 密码盐值
|
||||
Email string `xorm:"varchar(50) notnull unique default '' "` // 邮箱
|
||||
Created time.Time `xorm:"datetime notnull created"`
|
||||
Updated time.Time `xorm:"datetime updated"`
|
||||
Deleted time.Time `xorm:"datetime deleted"`
|
||||
IsAdmin int8 `xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户
|
||||
Status Status `xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (user *User) Create() (insertId int, err error) {
|
||||
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
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (user *User) Update(id int, data CommonMap) (int64, error) {
|
||||
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)
|
||||
|
||||
return user.Update(id, CommonMap{"password": safePassword, "salt": salt})
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (user *User) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(user)
|
||||
}
|
||||
|
||||
// 禁用
|
||||
func (user *User) Disable(id int) (int64, error) {
|
||||
return user.Update(id, CommonMap{"status": Disabled})
|
||||
}
|
||||
|
||||
// 激活
|
||||
func (user *User) Enable(id int) (int64, error) {
|
||||
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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 用户名是否存在
|
||||
func (user *User) UsernameExists(username string) (int64, error) {
|
||||
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) List() ([]User, error) {
|
||||
list := make([]User, 0)
|
||||
err := Db.Desc("id").Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (user *User) Total() (int64, error) {
|
||||
return Db.Count(user)
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
func (user *User) encryptPassword(password, salt string) string {
|
||||
return utils.Md5(password + salt)
|
||||
}
|
||||
|
||||
// 生成密码盐值
|
||||
func (user *User) generateSalt() string {
|
||||
return utils.RandString(PasswordSaltLength)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"runtime"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"gopkg.in/ini.v1"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
AppDir string // 应用根目录
|
||||
ConfDir string // 配置目录
|
||||
LogDir string // 日志目录
|
||||
DataDir string // 存放session等
|
||||
AppConfig string // 应用配置文件
|
||||
Installed bool // 应用是否安装过
|
||||
Setting *ini.Section // 应用配置
|
||||
PidFile string
|
||||
)
|
||||
|
||||
|
||||
func InitEnv() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
logger.InitLogger()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
AppDir = wd
|
||||
ConfDir = AppDir + "/conf"
|
||||
LogDir = AppDir + "/log"
|
||||
DataDir = AppDir + "/data"
|
||||
AppConfig = ConfDir + "/app.ini"
|
||||
PidFile = LogDir + "/gocron.pid"
|
||||
checkDirExists(ConfDir, LogDir, DataDir)
|
||||
Installed = IsInstalled()
|
||||
}
|
||||
|
||||
// 判断应用是否安装过
|
||||
func IsInstalled() bool {
|
||||
_, err := os.Stat(ConfDir + "/install.lock")
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func WritePid() {
|
||||
pid := os.Getpid()
|
||||
pidStr := strconv.Itoa(pid)
|
||||
err := ioutil.WriteFile(PidFile, []byte(pidStr), 0644)
|
||||
if err != nil {
|
||||
logger.Fatal("写入pid文件失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPid() int {
|
||||
bytes, err := ioutil.ReadFile(PidFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
pidStr := string(bytes)
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return pid
|
||||
}
|
||||
|
||||
func RemovePid() {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
|
||||
// 创建安装锁文件
|
||||
func CreateInstallLock() error {
|
||||
_, err := os.Create(ConfDir + "/install.lock")
|
||||
if err != nil {
|
||||
logger.Error("创建安装锁文件conf/install.lock失败")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 检测目录是否存在
|
||||
func checkDirExists(path ...string) {
|
||||
for _, value := range path {
|
||||
if !utils.FileExist(value) {
|
||||
logger.Fatal(value + "目录不存在或无权限访问")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package httpclient
|
||||
|
||||
// http-client
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
"fmt"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
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 createRequestError(err error) ResponseWrapper {
|
||||
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
|
||||
return ResponseWrapper{0, errorMessage, make(http.Header)}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/cihub/seelog"
|
||||
"gopkg.in/macaron.v1"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// 日志库
|
||||
|
||||
type Level int8
|
||||
|
||||
var logger seelog.LoggerInterface
|
||||
|
||||
const (
|
||||
DEBUG = iota
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
FATAL
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
write(DEBUG, 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 Warn(v ...interface{}) {
|
||||
write(WARN, 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 Fatal(v ...interface{}) {
|
||||
write(FATAL, v)
|
||||
}
|
||||
|
||||
func Fatalf(format string, v ...interface{}) {
|
||||
writef(FATAL, format, v...)
|
||||
}
|
||||
|
||||
func write(level Level, 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)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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 {
|
||||
config := `
|
||||
<seelog>
|
||||
<outputs formatid="main">
|
||||
%s
|
||||
<filter levels="info,critical,error,warn">
|
||||
<file path="log/cron.log" />
|
||||
</filter>
|
||||
</outputs>
|
||||
<formats>
|
||||
<format id="main" format="%%Date/%%Time [%%LEV] %%Msg%%n"/>
|
||||
</formats>
|
||||
</seelog>`
|
||||
|
||||
consoleConfig := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
consoleConfig =
|
||||
`
|
||||
<filter levels="info,debug,critical,warn,error">
|
||||
<console />
|
||||
</filter>
|
||||
`
|
||||
}
|
||||
config = fmt.Sprintf(config, consoleConfig)
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// @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(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)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Message map[string]interface{}
|
||||
|
||||
type Notifiable interface {
|
||||
Send(msg Message)
|
||||
}
|
||||
|
||||
var queue chan Message = make(chan Message, 100)
|
||||
|
||||
func init() {
|
||||
go run()
|
||||
}
|
||||
|
||||
// 把消息推入队列
|
||||
func Push(msg Message) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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, 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)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// 格式化消息内容
|
||||
func (slack *Slack) format(content string, channel string) string {
|
||||
content = utils.EscapeJson(content)
|
||||
specialChars := []string{"&", "<", ">"}
|
||||
replaceChars := []string{"&", "<", ">"}
|
||||
content = utils.ReplaceStrings(content, specialChars, replaceChars)
|
||||
|
||||
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
var (
|
||||
errUnavailable = errors.New("无法连接远程服务器")
|
||||
)
|
||||
|
||||
func Exec(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)
|
||||
}
|
||||
|
||||
return "", errUnavailable
|
||||
}
|
||||
|
||||
func exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("panic#rpc/client.go:Exec#", err)
|
||||
}
|
||||
} ()
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
conn, err := grpcpool.Pool.Get(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
isConnClosed := false
|
||||
defer func() {
|
||||
if !isConnClosed {
|
||||
grpcpool.Pool.Put(addr, conn)
|
||||
}
|
||||
}()
|
||||
c := pb.NewTaskClient(conn)
|
||||
if taskReq.Timeout <= 0 || taskReq.Timeout > 86400 {
|
||||
taskReq.Timeout = 86400
|
||||
}
|
||||
timeout := time.Duration(taskReq.Timeout) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
resp, err := c.Run(ctx, taskReq)
|
||||
if err != nil {
|
||||
return parseGRPCError(err, conn, &isConnClosed)
|
||||
}
|
||||
|
||||
if resp.Error == "" {
|
||||
return resp.Output, nil
|
||||
}
|
||||
|
||||
return resp.Output, errors.New(resp.Error)
|
||||
}
|
||||
|
||||
func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string, error) {
|
||||
switch grpc.Code(err) {
|
||||
case codes.Unavailable:
|
||||
conn.Close()
|
||||
*connClosed = true
|
||||
return "", errUnavailable
|
||||
case codes.DeadlineExceeded:
|
||||
return "", errors.New("执行超时, 强制结束")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package grpcpool
|
||||
|
||||
import (
|
||||
"github.com/silenceper/pool"
|
||||
"sync"
|
||||
"time"
|
||||
"google.golang.org/grpc"
|
||||
"errors"
|
||||
)
|
||||
|
||||
|
||||
var (
|
||||
Pool GRPCPool
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidConn = errors.New("invalid connection")
|
||||
)
|
||||
|
||||
func init() {
|
||||
Pool = GRPCPool{
|
||||
make(map[string]pool.Pool),
|
||||
sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
type GRPCPool struct {
|
||||
// map key格式 ip:port
|
||||
conns map[string]pool.Pool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *GRPCPool) Get(addr string) (*grpc.ClientConn, error) {
|
||||
p.RLock()
|
||||
p.RUnlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if !ok {
|
||||
err := p.newCommonPool(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pool = p.conns[addr]
|
||||
conn, err := pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn.(*grpc.ClientConn), nil
|
||||
}
|
||||
|
||||
func (p *GRPCPool) Put(addr string, conn *grpc.ClientConn) error {
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if ok {
|
||||
return pool.Put(conn)
|
||||
}
|
||||
|
||||
return ErrInvalidConn
|
||||
}
|
||||
|
||||
|
||||
// 释放连接池
|
||||
func (p *GRPCPool) Release(addr string) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pool.Release()
|
||||
delete(p.conns, addr)
|
||||
}
|
||||
|
||||
// 释放所有连接池
|
||||
func (p *GRPCPool) ReleaseAll() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
for _, pool := range(p.conns) {
|
||||
pool.Release()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化底层连接池
|
||||
func (p *GRPCPool) newCommonPool(addr string) (error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
commonPool, ok := p.conns[addr]
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
poolConfig := &pool.PoolConfig{
|
||||
InitialCap: 1,
|
||||
MaxCap: 30,
|
||||
Factory: func() (interface{}, error) {
|
||||
return grpc.Dial(addr, grpc.WithInsecure())
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
conn, ok := v.(*grpc.ClientConn)
|
||||
if ok && conn != nil {
|
||||
return conn.Close()
|
||||
}
|
||||
return ErrInvalidConn
|
||||
},
|
||||
IdleTimeout: 3 * time.Minute,
|
||||
}
|
||||
|
||||
commonPool, err := pool.NewChannelPool(poolConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.conns[addr] = commonPool
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type Server struct {}
|
||||
|
||||
func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
grpclog.Println(err)
|
||||
}
|
||||
} ()
|
||||
output, err := utils.ExecShell(ctx, req.Command)
|
||||
resp := new(pb.TaskResponse)
|
||||
resp.Output = output
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Error = ""
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func Start(addr string) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterTaskServer(s, Server{})
|
||||
grpclog.Println("listen address ", addr)
|
||||
err = s.Serve(l)
|
||||
if err != nil {
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const DefaultSection = "default"
|
||||
|
||||
// 读取配置
|
||||
func Read(filename string) (*ini.Section,error) {
|
||||
config, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
section := config.Section(DefaultSection)
|
||||
|
||||
return section, nil
|
||||
}
|
||||
|
||||
// 写入配置
|
||||
func Write(config []string, filename string) error {
|
||||
if len(config) == 0 {
|
||||
return errors.New("参数不能为空")
|
||||
}
|
||||
if len(config) % 2 != 0 {
|
||||
return errors.New("参数不匹配")
|
||||
}
|
||||
|
||||
file := ini.Empty()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"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"` // 数据
|
||||
}
|
||||
|
||||
type JsonResponse struct{}
|
||||
|
||||
const ResponseSuccess = 0
|
||||
const ResponseFailure = 1
|
||||
const NotFound = 2
|
||||
const AuthError = 3
|
||||
const ServerError = 4
|
||||
|
||||
const SuccessContent = "操作成功"
|
||||
const FailureContent = "操作失败"
|
||||
|
||||
func JsonResponseByErr(err error) string {
|
||||
json := JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure(FailureContent, err)
|
||||
}
|
||||
|
||||
return json.Success(SuccessContent, nil)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Success(message string, data interface{}) string {
|
||||
return j.response(ResponseSuccess, message, data)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Failure(code int, message string) string {
|
||||
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) response(code int, message string, data interface{}) string {
|
||||
resp := response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"time"
|
||||
"runtime"
|
||||
"github.com/Tang-RoseChild/mahonia"
|
||||
"strings"
|
||||
"os"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 生成长度为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)])
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// 生成32位MD5摘要
|
||||
func Md5(str string) string {
|
||||
m := md5.New()
|
||||
m.Write([]byte(str))
|
||||
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
// 生成0-max之间随机数
|
||||
func RandNumber(max int) int {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
return r.Intn(max)
|
||||
}
|
||||
|
||||
// 判断当前系统是否是windows
|
||||
func IsWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
// GBK编码转换为UTF8
|
||||
func GBK2UTF8(s string) (string, bool) {
|
||||
dec := mahonia.NewDecoder("gbk")
|
||||
|
||||
return dec.ConvertStringOK(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)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func InStringSlice(slice []string, element string) bool {
|
||||
element = strings.TrimSpace(element)
|
||||
for _, v := range slice {
|
||||
if strings.TrimSpace(v) == element{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 转义json特殊字符
|
||||
func EscapeJson(s string) string {
|
||||
specialChars := []string{"\\", "\b","\f", "\n", "\r", "\t", "\"",}
|
||||
replaceChars := []string{ "\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\"",}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 格式化环境变量
|
||||
func FormatUnixEnv(key, value string) string {
|
||||
return fmt.Sprintf("export %s=%s; ", key, value)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRandString(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandNumber(t *testing.T) {
|
||||
num := RandNumber(10000)
|
||||
if num <= 0 && num >= 10000 {
|
||||
t.Fatalf("随机数不在有效范围内-%d", num)
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"golang.org/x/net/context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"golang.org/x/net/context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
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 ConvertEncoding(outputGBK string) (string) {
|
||||
// windows平台编码为gbk,需转换为utf8才能入库
|
||||
outputUTF8, ok := GBK2UTF8(outputGBK)
|
||||
if ok {
|
||||
return outputUTF8
|
||||
}
|
||||
|
||||
return "命令输出转换编码失败(gbk to utf8)"
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 生成压缩包 xx.tar.gz或xx.zip
|
||||
# 使用 ./package.sh -a amd664 -p linux -v v2.0.0
|
||||
|
||||
# 任何命令返回非0值退出
|
||||
set -o errexit
|
||||
# 使用未定义的变量退出
|
||||
set -o nounset
|
||||
# 管道中任一命令执行失败退出
|
||||
set -o pipefail
|
||||
|
||||
eval $(go env)
|
||||
|
||||
# 二进制文件名
|
||||
BINARY_NAME=''
|
||||
# main函数所在文件
|
||||
MAIN_FILE=""
|
||||
|
||||
# 提取git最新tag作为应用版本
|
||||
VERSION=''
|
||||
# 最新git commit id
|
||||
GIT_COMMIT_ID=''
|
||||
|
||||
# 外部输入的系统
|
||||
INPUT_OS=()
|
||||
# 外部输入的架构
|
||||
INPUT_ARCH=()
|
||||
# 未指定OS,默认值
|
||||
DEFAULT_OS=${GOHOSTOS}
|
||||
# 未指定ARCH,默认值
|
||||
DEFAULT_ARCH=${GOHOSTARCH}
|
||||
# 支持的系统
|
||||
SUPPORT_OS=(linux darwin windows)
|
||||
# 支持的架构
|
||||
SUPPORT_ARCH=(386 amd64)
|
||||
|
||||
# 编译参数
|
||||
LDFLAGS=''
|
||||
# 需要打包的文件
|
||||
INCLUDE_FILE=()
|
||||
# 打包文件生成目录
|
||||
PACKAGE_DIR=''
|
||||
# 编译文件生成目录
|
||||
BUILD_DIR=''
|
||||
|
||||
# 获取git 最新tag name
|
||||
git_latest_tag() {
|
||||
local COMMIT_ID=""
|
||||
local TAG_NAME=""
|
||||
COMMIT_ID=`git rev-list --tags --max-count=1`
|
||||
TAG_NAME=`git describe --tags "${COMMIT_ID}"`
|
||||
|
||||
echo ${TAG_NAME}
|
||||
}
|
||||
|
||||
# 获取git 最新commit id
|
||||
git_latest_commit() {
|
||||
echo "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
# 打印信息
|
||||
print_message() {
|
||||
echo "$1"
|
||||
}
|
||||
|
||||
# 打印信息后推出
|
||||
print_message_and_exit() {
|
||||
if [[ -n $1 ]]; then
|
||||
print_message "$1"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 设置系统、CPU架构
|
||||
set_os_arch() {
|
||||
if [[ ${#INPUT_OS[@]} = 0 ]];then
|
||||
INPUT_OS=("${DEFAULT_OS}")
|
||||
fi
|
||||
|
||||
if [[ ${#INPUT_ARCH[@]} = 0 ]];then
|
||||
INPUT_ARCH=("${DEFAULT_ARCH}")
|
||||
fi
|
||||
|
||||
for OS in "${INPUT_OS[@]}"; do
|
||||
if [[ ! "${SUPPORT_OS[*]}" =~ ${OS} ]]; then
|
||||
print_message_and_exit "不支持的系统${OS}"
|
||||
fi
|
||||
done
|
||||
|
||||
for ARCH in "${INPUT_ARCH[@]}";do
|
||||
if [[ ! "${SUPPORT_ARCH[*]}" =~ ${ARCH} ]]; then
|
||||
print_message_and_exit "不支持的CPU架构${ARCH}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 初始化
|
||||
init() {
|
||||
set_os_arch
|
||||
|
||||
if [[ -z "${VERSION}" ]];then
|
||||
VERSION=`git_latest_tag`
|
||||
fi
|
||||
GIT_COMMIT_ID=`git_latest_commit`
|
||||
LDFLAGS="-w -X 'main.AppVersion=${VERSION}' -X 'main.BuildDate=`date '+%Y-%m-%d %H:%M:%S'`' -X 'main.GitCommit=${GIT_COMMIT_ID}'"
|
||||
|
||||
PACKAGE_DIR=${BINARY_NAME}-package
|
||||
BUILD_DIR=${BINARY_NAME}-build
|
||||
|
||||
if [[ -d ${BUILD_DIR} ]];then
|
||||
rm -rf ${BUILD_DIR}
|
||||
fi
|
||||
if [[ -d ${PACKAGE_DIR} ]];then
|
||||
rm -rf ${PACKAGE_DIR}
|
||||
fi
|
||||
|
||||
mkdir -p ${BUILD_DIR}
|
||||
mkdir -p ${PACKAGE_DIR}
|
||||
}
|
||||
|
||||
# 编译
|
||||
build() {
|
||||
local FILENAME=''
|
||||
for OS in "${INPUT_OS[@]}";do
|
||||
for ARCH in "${INPUT_ARCH[@]}";do
|
||||
if [[ "${OS}" = "windows" ]];then
|
||||
FILENAME=${BINARY_NAME}.exe
|
||||
else
|
||||
FILENAME=${BINARY_NAME}
|
||||
fi
|
||||
env CGO_ENABLED=0 GOOS=${OS} GOARCH=${ARCH} go build -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/${BINARY_NAME}-${OS}-${ARCH}/${FILENAME} ${MAIN_FILE}
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# 打包
|
||||
package_binary() {
|
||||
cd ${BUILD_DIR}
|
||||
|
||||
for OS in "${INPUT_OS[@]}";do
|
||||
for ARCH in "${INPUT_ARCH[@]}";do
|
||||
package_file ${BINARY_NAME}-${OS}-${ARCH}
|
||||
if [[ "${OS}" = "windows" ]];then
|
||||
zip -rq ../${PACKAGE_DIR}/${BINARY_NAME}-${VERSION}-${OS}-${ARCH}.zip ${BINARY_NAME}-${OS}-${ARCH}
|
||||
else
|
||||
tar czf ../${PACKAGE_DIR}/${BINARY_NAME}-${VERSION}-${OS}-${ARCH}.tar.gz ${BINARY_NAME}-${OS}-${ARCH}
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
cd ${OLDPWD}
|
||||
}
|
||||
|
||||
# 打包文件
|
||||
package_file() {
|
||||
if [[ "${#INCLUDE_FILE[@]}" = "0" ]];then
|
||||
return
|
||||
fi
|
||||
for item in "${INCLUDE_FILE[@]}"; do
|
||||
cp -r ../${item} $1
|
||||
done
|
||||
}
|
||||
|
||||
# 清理
|
||||
clean() {
|
||||
if [[ -d ${BUILD_DIR} ]];then
|
||||
rm -rf ${BUILD_DIR}
|
||||
fi
|
||||
}
|
||||
|
||||
# 运行
|
||||
run() {
|
||||
init
|
||||
build
|
||||
package_binary
|
||||
clean
|
||||
}
|
||||
|
||||
package_gocron() {
|
||||
BINARY_NAME='gocron'
|
||||
MAIN_FILE="./cmd/gocron/gocron.go"
|
||||
INCLUDE_FILE=()
|
||||
|
||||
|
||||
run
|
||||
}
|
||||
|
||||
package_gocron_node() {
|
||||
BINARY_NAME='gocron-node'
|
||||
MAIN_FILE="./cmd/node/node.go"
|
||||
INCLUDE_FILE=()
|
||||
|
||||
run
|
||||
}
|
||||
|
||||
# p 平台 linux darwin windows
|
||||
# a 架构 386 amd64
|
||||
# v 版本号 默认取git最新tag
|
||||
while getopts "p:a:v:" OPT;
|
||||
do
|
||||
case ${OPT} in
|
||||
p) IPS=',' read -r -a INPUT_OS <<< "${OPTARG}"
|
||||
;;
|
||||
a) IPS=',' read -r -a INPUT_ARCH <<< "${OPTARG}"
|
||||
;;
|
||||
v) VERSION=$OPTARG
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
package_gocron
|
||||
package_gocron_node
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
div.tagsinput { border: 1px solid rgba(0, 0, 0, 0.15); background: #FFF; padding:5px; width:300px; height:100px; overflow-y: auto; border-radius: 0.3125em;-webkit-box-shadow: 0em 0em 0em 0em rgba(0, 0, 0, 0.3) inset;box-shadow: 0em 0em 0em 0em rgba(0, 0, 0, 0.3) inset;-webkit-appearance: none;}
|
||||
div.tagsinput span.tag { border: 1px solid #a5d24a; -moz-border-radius:2px; -webkit-border-radius:2px; display: block; float: left; padding: 5px; text-decoration:none; background: #cde69c; color: #638421; margin-right: 5px; margin-bottom:5px;font-family: helvetica; font-size:13px;}
|
||||
div.tagsinput span.tag a { font-weight: bold; color: #82ad2b; text-decoration:none; font-size: 11px; }
|
||||
div.tagsinput input { width:120px !important; margin:0px;font-size: 13px; border:1px solid transparent; padding:5px; background: transparent; color: #000; outline:0px; margin-right:5px; margin-bottom:5px;
|
||||
-webkit-transition: background-color 0.3s ease-out, -webkit-box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
transition: background-color 0.3s ease-out, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
-webkit-box-shadow: 0em 0em 0em 0em rgba(0, 0, 0, 0.3) inset;
|
||||
box-shadow: 0em 0em 0em 0em rgba(0, 0, 0, 0.3) inset;
|
||||
-webkit-appearance: none;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
div.tagsinput input:focus {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
-webkit-box-shadow: 0.3em 0em 0em 0em rgba(0, 0, 0, 0.2) inset;
|
||||
box-shadow: 0.3em 0em 0em 0em rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
div.tagsinput div { display:block; float: left; }
|
||||
.tags_clear { clear: both; width: 100%; height: 0px; }
|
||||
.not_valid {background: #FBD8DB !important; color: #90111A !important;}
|
||||
@@ -1,418 +0,0 @@
|
||||
/*framework edit*/
|
||||
html{
|
||||
height:100%;
|
||||
font-size:100%;
|
||||
}
|
||||
body {
|
||||
font: 14px 'Helvetica Neue','Microsoft YaHei','Hiragino Sans GB','WenQuanYi Micro Hei',Helvetica,Arial,Sans-serif;
|
||||
line-height: 1.428571429;
|
||||
color: #444;
|
||||
margin:0px;
|
||||
padding:0px;
|
||||
height:100%;
|
||||
}
|
||||
ul,li{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
a {
|
||||
text-decoration:none;
|
||||
color:#666;
|
||||
}
|
||||
.ui.dropdown > .text {
|
||||
min-width:150px;
|
||||
}
|
||||
.ui.notif.menu {
|
||||
box-shadow:none;
|
||||
}
|
||||
.ui.notif.menu .item {
|
||||
padding:0 10px 0 0;
|
||||
margin-right:16px
|
||||
}
|
||||
.ui.notif.menu .item:before{
|
||||
display:none;
|
||||
}
|
||||
.ui.notif.menu .item:hover{
|
||||
color:#666;
|
||||
}
|
||||
/*the style begin*/
|
||||
.page{
|
||||
min-height:100%;
|
||||
width:100%;
|
||||
min-width:1200px;
|
||||
background: #fff;
|
||||
box-shadow: 0px 4px 15px #181818;
|
||||
padding-bottom:16px;
|
||||
padding-bottom:1rem;
|
||||
}
|
||||
span.ui.checkbox {
|
||||
padding-right:16px;
|
||||
}
|
||||
.ui.radio.checkbox{
|
||||
margin:16px 16px 8px 0 !important;
|
||||
}
|
||||
.ui.warning.message a{
|
||||
color:#96904D;
|
||||
}
|
||||
.ui.warning.message a:hover, .ui.warning.message a:focus {
|
||||
text-decoration:underline;
|
||||
}
|
||||
.ui.feed a{
|
||||
color: #009FDA;
|
||||
}
|
||||
.ui.feed a:hover,.ui.feed a:focus{
|
||||
color:#00BAFF;
|
||||
text-decoration:underline;
|
||||
}
|
||||
.ui.table {
|
||||
font-size:14px;
|
||||
}
|
||||
/*select*/
|
||||
select {
|
||||
text-transform: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.form-control {
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.428571429;
|
||||
vertical-align: middle;
|
||||
color: rgba(0,0,0,.8);
|
||||
background-color: #ffffff;
|
||||
background-image: none;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 4px;
|
||||
cursor:pointer;
|
||||
}
|
||||
.form-control:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.form-control:-moz-placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
.form-control::-moz-placeholder {
|
||||
color: #999999;
|
||||
opacity: 1;
|
||||
}
|
||||
.form-control:-ms-input-placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
.form-control::-webkit-input-placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
/*segment*/
|
||||
.ui.segment .right{
|
||||
float:right;
|
||||
}
|
||||
/*header*/
|
||||
header{
|
||||
position:relative;
|
||||
height:72px;
|
||||
}
|
||||
#logo{
|
||||
float:left;
|
||||
width:250px;
|
||||
height:72px;
|
||||
padding-top:15px;
|
||||
font-size:30px;
|
||||
}
|
||||
#logo a{
|
||||
display:block;
|
||||
width:180px;
|
||||
height:72px;
|
||||
text-indent: -999em;
|
||||
}
|
||||
header .user{
|
||||
display:inline;
|
||||
float:right;
|
||||
margin-top:22px;
|
||||
}
|
||||
/*main content*/
|
||||
.pageHeader, .verticalMenu{
|
||||
padding:16px 0;
|
||||
padding:1rem 0;
|
||||
}
|
||||
/*show api key*/
|
||||
#showApiKey .segment{
|
||||
background:none;
|
||||
box-shadow:none;
|
||||
-webkit-box-shadow:none;
|
||||
}
|
||||
#showApiKey .segment label{
|
||||
font-size:16px;
|
||||
}
|
||||
#deviceQuickButton {
|
||||
float:right;
|
||||
}
|
||||
/*index list*/
|
||||
#indexList{
|
||||
padding-top:2rem;
|
||||
}
|
||||
#indexList .segment{
|
||||
padding:0;
|
||||
}
|
||||
#indexList a.content{
|
||||
position:relative;
|
||||
padding:1rem;
|
||||
display:block;
|
||||
min-height:13em;
|
||||
z-index:100;
|
||||
}
|
||||
#indexList a.content:hover, #indexList a.content:focus{
|
||||
background-color:#b4ddf5 !important;
|
||||
}
|
||||
#indexList h4{
|
||||
margin:0;
|
||||
color:#333;
|
||||
}
|
||||
#indexList p.deviceId{
|
||||
margin-top:0;
|
||||
}
|
||||
#indexList p.deviceDescription{
|
||||
height:84px;
|
||||
overflow:auto;
|
||||
margin:2rem 0;
|
||||
}
|
||||
#indexList h4.newDevice{
|
||||
padding:4.7em 0;
|
||||
text-align:center;
|
||||
top:50%;
|
||||
}
|
||||
#indexList h4.newDevice i.icon{
|
||||
display:initial;
|
||||
}
|
||||
#indexList p{
|
||||
margin:0;
|
||||
font-size:14px;
|
||||
font-size:0.875rem;
|
||||
}
|
||||
/*the device list*/
|
||||
.ui.device.segment{
|
||||
clear: both !important;
|
||||
}
|
||||
.ui.device.segment p{
|
||||
color:#666;
|
||||
font-size:14px;
|
||||
font-size:0.875rem;
|
||||
margin:4px 0;
|
||||
margin:0.25rem 0;
|
||||
}
|
||||
.ui.device.segment span.stress{
|
||||
font-size:16px;
|
||||
font-size:1rem;
|
||||
color:#333;
|
||||
}
|
||||
.ui.device.segment .verborder{
|
||||
border-left: 3px solid #007ad5;
|
||||
}
|
||||
.ui.device.segment p.sensorStatus{
|
||||
font-size:14px;
|
||||
font-size:0.875rem;
|
||||
}
|
||||
.ui.device.segment .sub.header{
|
||||
line-height:1.4;
|
||||
}
|
||||
/*new device*/
|
||||
.locmap {
|
||||
height: 180px;
|
||||
background-color: #CCC;
|
||||
text-align: center;
|
||||
width: 100% !important;
|
||||
border: #CCC 1px solid;z
|
||||
}
|
||||
#location label{
|
||||
margin: 0em 0em 0.3em;
|
||||
display: block;
|
||||
color: #555555;
|
||||
font-size:14px;
|
||||
font-size:0.875rem;
|
||||
}
|
||||
#location input{
|
||||
padding:0!important;
|
||||
border: 1px solid transparent!important;
|
||||
box-shadow:none!important;
|
||||
-webkit-box-shadow:none!important;
|
||||
}
|
||||
/*the device manage page*/
|
||||
.ui.small.header{
|
||||
font-weight:normal;
|
||||
}
|
||||
.ui.grid.segment.sensor.segment{
|
||||
padding:32px 0 16px;
|
||||
padding:2rem 0 1rem;
|
||||
border-bottom:1px solid rgba(0,0,0,.1);
|
||||
}
|
||||
.ui.device.segment a.externalUrl {
|
||||
color:#007ad5;
|
||||
}
|
||||
.ui.device.segment .sensorAction{
|
||||
margin-top:8px;
|
||||
margin-top:0.5rem;
|
||||
}
|
||||
.ui.device.segment a.sensorEdit{
|
||||
margin-right:14px;
|
||||
margin-right:0.875rem;
|
||||
}
|
||||
.ui.device.segment a.sensorEdit:hover, .ui.basic.attached.accordion .content a:hover,.ui.device.segment a.sensorEdit:focus, .ui.basic.attached.accordion .content a:focus{
|
||||
color:#333;
|
||||
}
|
||||
.ui.device.segment a:hover, .ui.device.segment a:focus{
|
||||
cursor:pointer;
|
||||
}
|
||||
.ui.device.segment a.externalUrl:hover, .ui.device.segment a.externalUrl:focus{
|
||||
text-decoration:underline;
|
||||
}
|
||||
.ui.vertical.center.segment{
|
||||
text-align:center;
|
||||
}
|
||||
.ui.data.segment{
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
line-height:1.2;
|
||||
text-align:center;
|
||||
margin-top:22px !important;
|
||||
}
|
||||
.ui.data.segment p.data{
|
||||
font-size:48px;
|
||||
font-size:3rem;
|
||||
}
|
||||
.ui.data.segment p.gps{
|
||||
font-size:32px;
|
||||
font-size:2rem;
|
||||
}
|
||||
.ui.data.segment p.unit{
|
||||
font-size:12px;
|
||||
font-size:0.75rem;
|
||||
margin:0;
|
||||
}
|
||||
.ui.info.segment{
|
||||
padding-bottom:8px;
|
||||
padding-bottom:0.5rem;
|
||||
}
|
||||
/* ui header*/
|
||||
h5.ui.header.sprite-icon {
|
||||
padding-left:22px;
|
||||
background:url(../images/icon-sprite.png) no-repeat top left;
|
||||
}
|
||||
h5.ui.header.sprite-icon.data{ background-position: 0 -68px;}
|
||||
h5.ui.header.sprite-icon.bird{ background-position: 0 0;}
|
||||
h5.ui.header.sprite-icon.gps{ background-position: 0 -136px;}
|
||||
h5.ui.header.sprite-icon.key{ background-position: 0 -204px; }
|
||||
h5.ui.header.sprite-icon.pic{ background-position: 0 -268px;}
|
||||
h5.ui.header.sprite-icon.switchicon{ background-position: 0 -336px;}
|
||||
/*label*/
|
||||
.ui.attached.label.show{
|
||||
z-index:99;
|
||||
background: transparent;
|
||||
color:#333;
|
||||
right:14px !important;
|
||||
top:8px;
|
||||
padding-bottom: 2px;
|
||||
border-radius: 0!important;
|
||||
}
|
||||
.ui.attached.label.show.active{
|
||||
background:#fff;
|
||||
color: #333;
|
||||
border-style:solid;
|
||||
border-color:#ccc;
|
||||
border-width:1px 1px 0px;
|
||||
z-index:9999;
|
||||
}
|
||||
.ui.type.label{
|
||||
background-color:#47a447;
|
||||
border-color:#47a447;
|
||||
color:#fff;`
|
||||
font-size:12px;
|
||||
font-size:0.75rem;
|
||||
padding:0 7px!important;
|
||||
border-radius:.325em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ui.label.error{
|
||||
border-color:#D24B4C;
|
||||
color:#fff;
|
||||
}
|
||||
/*accordion*/
|
||||
.ui.basic.attached.accordion{
|
||||
width:inherit;
|
||||
padding:0 28px 14px;
|
||||
}
|
||||
.ui.basic.attached.accordion.showChart .title{
|
||||
font-size:12px;
|
||||
font-size:0.75rem;
|
||||
position:absolute;
|
||||
right:32px;
|
||||
z-index:999;
|
||||
}
|
||||
.ui.basic.attached.accordion.action .title{
|
||||
margin:0;
|
||||
padding-top:0;
|
||||
}
|
||||
.ui.basic.attached.accordion.action .content{
|
||||
margin-top:8px;
|
||||
/* border-top:1px solid rgba(204, 204, 204, 0.3);*/
|
||||
}
|
||||
/*chart*/
|
||||
.chartShow{
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
right: 14px;
|
||||
width: 96%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
.active.chartShow{
|
||||
display:block;
|
||||
}
|
||||
/*image box*/
|
||||
.field.devimage{
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.dev_img_inner {
|
||||
width:7777px;
|
||||
overflow: hidden;
|
||||
}
|
||||
/*footer start*/
|
||||
footer {
|
||||
width:100%;
|
||||
height:150px;
|
||||
min-width:1200px;
|
||||
background:#323232;
|
||||
position:absolute;
|
||||
z-index: -20;
|
||||
}
|
||||
#copyrights {
|
||||
margin:0 auto;
|
||||
height:140px;
|
||||
position:fixed;
|
||||
left:0px;
|
||||
right:0px;
|
||||
bottom:0px;
|
||||
z-index: -15;
|
||||
}
|
||||
#copyrights .inset {
|
||||
margin:0 auto;
|
||||
padding:20px 50px 0;
|
||||
position:relative;
|
||||
}
|
||||
#copyrights .logo {
|
||||
width:164px;
|
||||
height:40px;
|
||||
background:url(../images/logo_footer.png);
|
||||
text-indent:-999em;
|
||||
}
|
||||
#copyrights p {
|
||||
font-size:12px;
|
||||
color:#a0a0a0;
|
||||
line-height:24px;
|
||||
}
|
||||
#copyrights .fl {
|
||||
float:left;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
label.switch {
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
input#switch {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
.switch {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 5.5em;
|
||||
height: 3em;
|
||||
cursor: pointer;
|
||||
border-radius: 1.5em;
|
||||
-webkit-transition: 350ms;
|
||||
transition: 350ms;
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.07)), to(rgba(255, 255, 255, 0))), #dddddd;
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0)), #dddddd;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0)), #dddddd;
|
||||
-webkit-box-shadow: 0 0.07em 0.1em -0.1em rgba(0, 0, 0, 0.4) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 0.07em 0.1em -0.1em rgba(0, 0, 0, 0.4) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.switch::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
border-radius: 50%;
|
||||
-webkit-transition: 250ms ease-in-out;
|
||||
transition: 250ms ease-in-out;
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#eeeeee));
|
||||
background: -webkit-linear-gradient(#f5f5f5 10%, #eeeeee);
|
||||
background: linear-gradient(#f5f5f5 10%, #eeeeee);
|
||||
-webkit-box-shadow: 0 0.1em 0.15em -0.05em rgba(255, 255, 255, 0.9) inset, 0 0.2em 0.2em -0.12em rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 0.1em 0.15em -0.05em rgba(255, 255, 255, 0.9) inset, 0 0.2em 0.2em -0.12em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.switch::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 4em;
|
||||
height: 1.5em;
|
||||
top: 0.75em;
|
||||
left: 0.75em;
|
||||
border-radius: 0.75em;
|
||||
-webkit-transition: 250ms ease-in-out;
|
||||
transition: 250ms ease-in-out;
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.07)), to(rgba(255, 255, 255, 0.1))), #d0d0d0;
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.1)), #d0d0d0;
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.07)), to(rgba(255, 255, 255, 0.1))), #ec2727;
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.1)), #ec2727;
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.07)), to(rgba(255, 255, 255, 0.1))), #D95C5C;
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.1)), #D95C5C;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.1)), #D95C5C;
|
||||
-webkit-box-shadow: 0 0.08em 0.15em -0.1em rgba(0, 0, 0, 0.5) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7), 0 0 0 0 rgba(68, 204, 102, 0.7) inset;
|
||||
box-shadow: 0 0.08em 0.15em -0.1em rgba(0, 0, 0, 0.5) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7), 0 0 0 0 rgba(68, 204, 102, 0.7) inset;
|
||||
}
|
||||
input:checked + .switch::before {
|
||||
-webkit-box-shadow: 0 0.08em 0.15em -0.1em rgba(0, 0, 0, 0.5) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7), 3em 0 0 0 rgba(68, 204, 102, 0.7) inset;
|
||||
box-shadow: 0 0.08em 0.15em -0.1em rgba(0, 0, 0, 0.5) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7), 3em 0 0 0 rgba(68, 204, 102, 0.7) inset;
|
||||
}
|
||||
input:checked + .switch::after {
|
||||
left: 3em;
|
||||
}
|
||||
Binary file not shown.
@@ -1,450 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>
|
||||
Created by FontForge 20100429 at Thu Sep 20 22:09:47 2012
|
||||
By root
|
||||
Copyright (C) 2012 by original authors @ fontello.com
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="icons" horiz-adv-x="947" >
|
||||
<font-face
|
||||
font-family="icons"
|
||||
font-weight="500"
|
||||
font-stretch="normal"
|
||||
units-per-em="1000"
|
||||
panose-1="2 0 6 3 0 0 0 0 0 0"
|
||||
ascent="800"
|
||||
descent="-200"
|
||||
bbox="-0.666667 -200.023 1350 801.8"
|
||||
underline-thickness="50"
|
||||
underline-position="-100"
|
||||
unicode-range="U+002B-1F6AB"
|
||||
/>
|
||||
<missing-glyph horiz-adv-x="364"
|
||||
d="M33 0v666h265v-666h-265zM66 33h199v600h-199v-600z" />
|
||||
<glyph glyph-name=".notdef" horiz-adv-x="364"
|
||||
d="M33 0v666h265v-666h-265zM66 33h199v600h-199v-600z" />
|
||||
<glyph glyph-name=".null" horiz-adv-x="0"
|
||||
/>
|
||||
<glyph glyph-name="nonmarkingreturn" horiz-adv-x="333"
|
||||
/>
|
||||
<glyph glyph-name="plus" unicode="+" horiz-adv-x="610"
|
||||
d="M565 350q19 0 24.5 -12.5t5.5 -37.5q0 -17 -1.5 -26t-8.5 -16.5t-20 -7.5h-210v-210q0 -19 -12.5 -24.5t-37.5 -5.5q-17 0 -26 1.5t-16.5 8.5t-7.5 20v210h-210q-19 0 -24.5 12.5t-5.5 37.5q0 17 1.5 26t8.5 16.5t20 7.5h210v210q0 19 12.5 24.5t37.5 5.5q17 0 26 -1.5
|
||||
t16.5 -8.5t7.5 -20v-210h210z" />
|
||||
<glyph glyph-name="hyphen" unicode="-" horiz-adv-x="610"
|
||||
d="M565 350q19 0 24.5 -12.5t5.5 -37.5q0 -17 -1.5 -26t-8.5 -16.5t-20 -7.5h-520q-19 0 -24.5 12.5t-5.5 37.5q0 17 1.5 26t8.5 16.5t20 7.5h520z" />
|
||||
<glyph glyph-name="at" unicode="@" horiz-adv-x="923"
|
||||
d="M849 516q25 -64 25 -138q0 -103 -46 -182q-59 -92 -163 -92q-43 0 -70 21q-21 15 -30 36q-43 -55 -114 -55q-81 0 -122 58q-41 52 -41 141q0 91 48 147q53 64 142 64q52 0 90 -37l7 26h85l-18 -259q-2 -32 9 -44q7 -7 20 -7q35 0 60 34.5t34 73.5t9 75q0 108 -68 176
|
||||
q-75 76 -208 76q-128 0 -214 -76q-102 -92 -102 -245q0 -133 86 -221q81 -82 204 -82q137 0 242 46l15 8v-96l-7 -3q-101 -50 -251 -50q-165 0 -269 97q-123 112 -123 308q0 182 116 294q57 56 133 84q77 31 168 31q178 0 284 -106q46 -44 69 -103zM465 199q46 0 69 40
|
||||
q17 31 17 85q0 45 -16 74q-20 28 -60 28q-44 0 -64 -38q-19 -33 -19 -83q0 -23 4 -43.5t21.5 -41.5t47.5 -21z" />
|
||||
<glyph glyph-name="h" unicode="h" horiz-adv-x="1228"
|
||||
d="M281 54q0 56 32 138q56 164 76 215q31 82 51 144h-25h-31h-26q-51 10 -51 51q0 51 51 51h564q51 0 51 -51q0 -41 -36 -47q-15 -5 -31 -4h-15h-20h-32q11 -41 52 -144q61 -164 77 -210q30 -92 30 -143q0 -46 -40 -87q-31 -31 -87 -31h-461q-51 0 -93 31q-36 46 -36 87z
|
||||
M481 345h318l-67 206h-184z" />
|
||||
<glyph glyph-name="uni2139" unicode="ℹ" horiz-adv-x="490"
|
||||
d="M367 800q48 0 74 -28t26 -70q0 -50 -39 -88t-95 -38q-47 0 -73.5 27t-25.5 73q0 45 36 84.5t97 39.5zM160 -200q-29 0 -45.5 13.5t-22.5 52.5t14 110l60 255q15 57 0 57q-13 0 -54.5 -18t-70.5 -38l-26 44q91 77 190 125t150 48q26 0 38.5 -20t11 -55.5t-14.5 -85.5
|
||||
l-69 -268q-16 -63 5 -63q15 0 49.5 16t69.5 44l30 -41q-84 -86 -175 -131t-140 -45z" />
|
||||
<glyph glyph-name="arrowleft" unicode="←" horiz-adv-x="789"
|
||||
d="M0 329q0 20 15 35l345 346q15 15 35 15t34 -15l52 -52q15 -14 15 -34t-15 -35l-174 -174h433q21 0 35 -14t14 -35v-74q0 -20 -14 -34t-34 -14h-434l174 -174q15 -15 15 -35t-15 -34l-52 -52q-14 -15 -34 -15t-35 15l-345 345q-15 15 -15 35z" />
|
||||
<glyph glyph-name="arrowup" unicode="↑" horiz-adv-x="789"
|
||||
d="M-0.5 329q-0.5 20 14.5 35l346 345q15 15 35 15t34 -15l346 -345q14 -15 14 -35t-14 -35l-52 -51q-15 -15 -34.5 -15t-33.5 15l-175 173v-433q0 -20 -14 -34.5t-34 -14.5h-74q-21 0 -35 14.5t-14 34.5v433l-173 -173q-15 -15 -35 -15t-35 15l-51 51q-15 15 -15.5 35z" />
|
||||
<glyph glyph-name="arrowright" unicode="→" horiz-adv-x="789"
|
||||
d="M0 293v74q0 20 14 34t34 14h434l-174 174q-14 15 -14 35t14 34l52 52q15 15 35 15t34 -15l346 -346q14 -15 14 -34.5t-14 -34.5l-346 -346q-14 -15 -34 -15t-35 15l-52 52q-14 14 -14 34t14 35l174 174h-434q-20 0 -34 14t-14 35z" />
|
||||
<glyph glyph-name="arrowdown" unicode="↓" horiz-adv-x="789"
|
||||
d="M0 328q0 20 15 35l51 51q15 15 35 15t35 -15l173 -173v433q0 20 14.5 34.5t34.5 14.5h74q21 0 34.5 -14.5t13.5 -34.5v-433l175 173q14 15 33.5 15t34.5 -15l53 -51q15 -15 15 -35t-15 -35l-347 -345q-14 -15 -34 -15t-35 15l-345 345q-15 15 -15 35z" />
|
||||
<glyph glyph-name="house" unicode="⌂" horiz-adv-x="930"
|
||||
d="M903 285q16 -16 11 -27.5t-28 -11.5h-83v-308q0 -14 -1.5 -21t-8.5 -13.5t-22 -6.5h-204v310h-204v-310h-195q-19 0 -28.5 6.5t-11 13.5t-1.5 21v308h-83q-23 0 -28 11.5t11 27.5l401 401q16 16 37 16t37 -16z" />
|
||||
<glyph glyph-name="uni25B4" unicode="▴" horiz-adv-x="490"
|
||||
d="M15 100l230 400l230 -400h-460z" />
|
||||
<glyph glyph-name="uni25B8" unicode="▸" horiz-adv-x="430"
|
||||
d="M15 530l400 -230l-400 -230v460z" />
|
||||
<glyph glyph-name="uni25BE" unicode="▾" horiz-adv-x="490"
|
||||
d="M475 500l-230 -400l-230 400h460z" />
|
||||
<glyph glyph-name="uni25C2" unicode="◂" horiz-adv-x="430"
|
||||
d="M415 530v-460l-400 230z" />
|
||||
<glyph glyph-name="uni2601" unicode="☁" horiz-adv-x="1291"
|
||||
d="M1292 155q0 -100 -68 -173.5t-167 -80.5v-1h-783v1q-7 -1 -19 -1q-106 0 -180.5 74.5t-74.5 180.5q0 66 33.5 124.5t90.5 92.5q-7 29 -7 56q0 106 74.5 180.5t180.5 74.5q92 0 167 -63q39 82 116 131t167 49q129 0 221 -92t92 -221q0 -50 -15 -93q77 -26 124.5 -92.5
|
||||
t47.5 -146.5z" />
|
||||
<glyph glyph-name="uni2611" unicode="☑"
|
||||
d="M0 92v474q0 32 12.5 61t33.5 50t50 34t62 13h553q13 0 26 -3l-96 -96h-483q-24 0 -41.5 -17.5t-17.5 -41.5v-474q0 -24 17.5 -41.5t41.5 -17.5h553q24 0 41 17.5t17 41.5v167l99 99v-266q0 -33 -12.5 -61t-34 -50t-50 -34.5t-60.5 -12.5h-553q-33 0 -62 12.5t-50 34.5
|
||||
t-33.5 50t-12.5 61zM198 431q0 17 11 28l51 51q12 12 28.5 12t28.5 -12l175 -175l335 337q12 12 29.5 12t28.5 -12l51 -50q11 -12 11 -29t-11 -28l-365 -366l-51 -50q-12 -12 -28.5 -12t-28.5 12l-51 50l-203 204q-11 11 -11 28z" />
|
||||
<glyph glyph-name="uni2661" unicode="♡" horiz-adv-x="880"
|
||||
d="M795 591q70 -65 70 -156t-70 -156l-355 -330l-355 330q-70 65 -70 156t70 156q62 58 149.5 58t149.5 -58l56 -52l56 52q62 58 149.5 58t149.5 -58zM743 330q42 38 42 105t-37 100q-42 39 -102 39q-49 0 -102 -49l-104 -91l-104 91q-53 49 -102 49q-60 0 -102 -39
|
||||
q-37 -33 -37 -100t42 -105l303 -286z" />
|
||||
<glyph glyph-name="heart" unicode="♥" horiz-adv-x="890"
|
||||
d="M804 591q70 -65 70 -156t-70 -156l-359 -330l-359 330q-70 65 -70 156t70 156q63 58 151 58t151 -58l57 -52l57 52q63 58 151 58t151 -58z" />
|
||||
<glyph glyph-name="uni2691" unicode="⚑"
|
||||
d="M0 645q0 32 23 55.5t56 23.5t56 -23.5t23 -55.5q0 -21 -10.5 -38.5t-28.5 -29.5v-623q0 -8 -6 -14t-14 -6h-40q-8 0 -13.5 6t-5.5 14v623q-18 12 -29 29.5t-11 38.5zM158 148v383q0 17 10 34t25 25q53 28 96.5 44t73.5 23q36 9 64 10q35 0 64 -6t55.5 -15.5t51.5 -21.5
|
||||
l50 -27q33 -14 75 -16q36 -3 84.5 7t106.5 45q14 9 23.5 3.5t9.5 -22.5v-384q0 -16 -9.5 -33.5t-23.5 -25.5q-58 -35 -106.5 -45t-84.5 -8q-42 3 -75 17l-50 27q-25 12 -51.5 21.5t-55.5 15.5t-64 6q-28 -1 -64 -10q-30 -7 -73.5 -23t-96.5 -45q-15 -9 -25 -2.5t-10 23.5z
|
||||
" />
|
||||
<glyph glyph-name="uni2699" unicode="⚙" horiz-adv-x="787"
|
||||
d="M0 271v117q0 7 7 9q20 6 42 9t43 5q4 0 8 0.5t9 1.5l11 29q6 14 14 29l-28 39q-15 20 -32 39q-6 5 0 13q20 24 42 46t46 42q5 5 13 0q11 -12 23.5 -21.5t24.5 -17.5q8 -5 15 -11t15 -11q27 15 58 24q3 30 6 54.5t8 47.5q2 9 10 9h117q9 0 9 -9q4 -20 7 -40l6 -41l3 -21
|
||||
q15 -5 29 -10.5t28 -13.5l14 10l12 10l28 19q13 10 26 22q6 6 12 -1l12 -12q5 -5 11 -10l32 -31l31 -34q4 -7 0 -13q-14 -16 -28 -35l-31 -42q15 -30 25 -61q13 -3 26 -4.5t27 -3.5q11 -2 24 -4l25 -4q7 -2 7 -9v-117q0 -7 -7 -10q-19 -5 -40 -7.5t-42 -4.5q-5 -1 -10 -1.5
|
||||
t-10 -1.5q-5 -15 -11 -29l-13 -29q12 -18 27.5 -38.5t32.5 -39.5q5 -5 0 -13q-40 -49 -89 -88q-5 -5 -12 0q-12 11 -24 20.5t-25 18.5q-7 5 -14.5 11t-14.5 11q-28 -15 -58 -24q-2 -25 -6 -51t-10 -51q-2 -9 -9 -9h-117q-8 0 -10 9q-3 20 -5.5 40t-5.5 41l-3 21l-29 10
|
||||
q-15 5 -28 14l-14 -10l-13 -10q-27 -19 -53 -41q-7 -6 -13 1l-11 11l-12 11l-32 31q-15 16 -30 34q-5 7 0 13q16 19 30 39l29 38q-16 30 -26 61q-12 3 -25 4.5t-26 3.5t-26 3.5t-25 4.5q-7 2 -7 9zM275 329q0 -25 9.5 -46.5t25.5 -37.5t37.5 -25.5t46 -9.5t46 9.5t37.5 25.5
|
||||
t25.5 37.5t9.5 46.5q0 24 -9.5 45.5t-25.5 37.5t-37.5 25.5t-46 9.5t-46 -9.5t-37.5 -25.5t-25.5 -37.5t-9.5 -45.5z" />
|
||||
<glyph glyph-name="uni26A0" unicode="⚠" horiz-adv-x="894"
|
||||
d="M5.5 -41q-14.5 25 6.5 59l387 671q19 35 48 35q28 0 49 -35l387 -671q20 -34 6 -59t-54 -25h-775q-40 0 -54.5 25zM168 53h558l-279 483zM389 391q0 6 4.5 10.5t9.5 4.5h89q5 0 9 -4.5t4 -10.5l-7 -192q0 -12 -14 -12h-73q-14 0 -14 12zM392 133q0 14 13 14h82
|
||||
q14 0 14 -14l1 -51q0 -14 -14 -14h-82q-13 0 -13 14z" />
|
||||
<glyph glyph-name="uni26A1" unicode="⚡" horiz-adv-x="430"
|
||||
d="M55 -150q-4 3 35 92l79 182q40 93 38 99t-94.5 45t-97.5 54q-4 12 84 120.5t179.5 210t96.5 97.5t-74.5 -186t-77.5 -188q1 -3 95 -42.5t97 -56.5q3 -20 -174 -226t-186 -201z" />
|
||||
<glyph glyph-name="uni26EF" unicode="⛯"
|
||||
d="M0 346v88q0 7 5 7q15 4 31 6l32 4q5 0 8 0.5t7 0.5q6 22 18 44q-9 15 -21 30l-24 29q-5 6 0 10q15 18 31.5 35t34.5 32q6 4 10 -1q8 -8 18 -15l18 -14l23 -16q22 11 44 18q2 22 4.5 41t6.5 37q0 5 7 5h88q7 0 7 -6q3 -15 5.5 -30.5t4.5 -31.5l2 -15q21 -7 43 -18l20 15
|
||||
l21 15q10 8 19 16q6 4 10 -1q5 -4 9 -8l9 -9l23 -23l24 -26q3 -6 0 -10q-11 -11 -22 -25l-23 -33q6 -11 11 -23l9 -23q8 -2 18 -3t21 -3l19 -2q9 -1 17 -4q7 -2 7 -7v-88q0 -6 -6 -8q-14 -3 -31 -5l-32 -4q-4 0 -14 -2q-7 -21 -18 -43q9 -15 20.5 -30t24.5 -29q4 -6 0 -10
|
||||
q-15 -18 -31.5 -35t-34.5 -32q-7 -4 -10 1q-8 7 -17.5 14.5t-19.5 13.5q-5 6 -11 9.5t-11 7.5q-22 -11 -44 -18q-2 -18 -4 -38.5t-8 -39.5q-2 -5 -7 -5h-88q-7 0 -7 5q-3 15 -5 31l-4 31l-2 16q-21 7 -43 18l-10 -7q-5 -3 -10 -8l-21 -15q-10 -8 -19 -16q-7 -4 -10 1
|
||||
q-5 4 -8 8l-10 9l-24 22q-12 12 -23 27q-4 4 0 9q12 14 23.5 29.5t20.5 29.5q-10 21 -19 46q-8 2 -18 3t-21 3l-19 2q-9 1 -19 3q-5 3 -5 8zM207 389q0 -37 26.5 -63.5t63.5 -26.5t63 26.5t26 63.5t-26 63t-63 26t-63.5 -26t-26.5 -63zM552 132q-3 6 4 9l22 8q10 4 22 8
|
||||
q1 6 1.5 10t2.5 9.5t3.5 9t4.5 8.5q-8 11 -14 21t-13 20q-3 6 2 9l66 59q4 4 9 1l18 -15l19 -16q19 8 37 9q5 11 11 21.5t11 20.5q3 5 8 3l85 -26q5 -3 5 -9q-2 -11 -4.5 -22.5t-4.5 -22.5q9 -6 15 -13.5t12 -16.5q12 1 23.5 1.5t23.5 0.5q5 0 7 -5l19 -87q2 -5 -4 -8
|
||||
q-11 -5 -21 -9l-23 -7q-1 -6 -1.5 -10t-2.5 -9.5t-4 -9t-4 -7.5l15 -21l12 -20q2 -5 -2 -9l-66 -60q-4 -4 -9 0q-9 8 -18.5 15t-17.5 15q-21 -8 -39 -9l-10 -22l-11 -20q-3 -5 -8 -3l-85 26q-5 3 -5 9q2 11 3.5 23t4.5 23q-15 13 -26 29q-12 -2 -24.5 -3t-23.5 1q-5 0 -8 5z
|
||||
M589 573q0 5 6 8q10 2 20.5 4.5t21.5 4.5q2 4 3 8.5t3 8.5t5 7t5 8q-6 10 -10 20l-8 20q-2 4 2 8l67 44q5 3 9 -1q8 -7 15 -15l14 -16q17 3 35 3q12 19 25 34q3 4 9 3l72 -36q5 -3 3 -9q-2 -10 -5.5 -19.5t-6.5 -20.5q10 -13 19 -30q11 -1 22 -1.5t22 -2.5q5 -2 5 -7l5 -81
|
||||
q0 -4 -5 -6q-10 -2 -20 -5l-22 -5q-2 -4 -3 -7.5t-3 -7.5q-3 -8 -9 -15q6 -11 10 -21t8 -19q2 -5 -3 -8l-66 -45q-6 -3 -9 1q-13 13 -29 32q-9 -2 -18 -3t-18 0q-6 -10 -12 -19l-13 -17q-3 -3 -9 -1l-72 36q-6 2 -3 7l6 21q3 10 7 20q-6 6 -11 13.5t-9 16.5q-11 1 -22 1.5
|
||||
t-22 2.5q-5 0 -5 6zM693 148q-8 -23 3 -44.5t36 -29.5q23 -8 44.5 3t28.5 35q9 23 -2 44.5t-36 30.5q-23 7 -44.5 -4t-29.5 -35zM716.5 528q7.5 -21 27.5 -32q21 -9 42 -2t31 27q10 21 3 42t-27 30q-20 11 -41 3.5t-32 -27.5t-3.5 -41z" />
|
||||
<glyph glyph-name="uni2708" unicode="✈" horiz-adv-x="1030"
|
||||
d="M282 -170l125 400h-179l-113 -100h-100l80 170l-80 170h100l113 -100h179l-125 400h100l225 -400h258q6 0 16 -0.5t36 -4t46 -10.5t36 -21t16 -34q0 -31 -37.5 -48.5t-75.5 -19.5l-37 -2h-258l-225 -400h-100z" />
|
||||
<glyph glyph-name="uni2709" unicode="✉" horiz-adv-x="930"
|
||||
d="M45 536q-23 12 -28 33q1 19 25 21h846q33 0 23 -25q-7 -19 -26 -29l-375 -202q-19 -10 -45 -10t-45 10zM896 436q15 4 17 1.5t2 -12.5v-367q0 -16 -17 -32t-33 -16h-800q-16 0 -33 16t-17 32v367q0 10 2 12.5t17 -1.5l386 -202q19 -10 45 -10t45 10z" />
|
||||
<glyph glyph-name="uni270D" unicode="✍"
|
||||
d="M0 92v474q0 32 12.5 61t33.5 50t50 34t62 13h553q2 0 5 -0.5t5 -0.5l-98 -98h-465q-24 0 -41.5 -17.5t-17.5 -41.5v-474q0 -24 17.5 -41.5t41.5 -17.5h553q24 0 41 17.5t17 41.5v229l99 98v-327q0 -33 -12.5 -61t-34 -50t-50 -34.5t-60.5 -12.5h-553q-33 0 -62 12.5
|
||||
t-50 34.5t-33.5 50t-12.5 61zM324 101l56 169l335 335l113 -114l-334 -335zM445 257q3 -4 8.5 -4t8.5 4l263 262q10 10 0.5 18.5t-18.5 -0.5l-262 -262q-9 -9 0 -18zM772 662l47 48q15 15 35 15t33 -15l24 -23l23 -23q13 -15 13.5 -34.5t-13.5 -34.5l-49 -47z" />
|
||||
<glyph glyph-name="uni2715" unicode="✕" horiz-adv-x="500"
|
||||
d="M467 142q17 -17 17 -42t-17 -42t-42 -17t-42 17l-133 151l-133 -151q-17 -17 -42 -17t-42 17t-17 42t17 42l137 158l-137 158q-17 17 -17 42t17 42t42 17t42 -17l133 -151l133 151q17 17 42 17t42 -17t17 -42t-17 -42l-137 -158z" />
|
||||
<glyph glyph-name="uni2716" unicode="✖" horiz-adv-x="870"
|
||||
d="M435 720q174 0 297 -123t123 -297t-123 -297t-297 -123t-297 123t-123 297t123 297t297 123zM521 300l153 153l-86 86l-153 -153l-153 153l-86 -86l153 -153l-153 -153l86 -87l153 154l153 -153l86 86z" />
|
||||
<glyph glyph-name="uni2731" unicode="✱" horiz-adv-x="733"
|
||||
d="M1 457q-5 19 5 37l37 64q11 18 30.5 23t37.5 -5l170 -99v197q0 21 14 35.5t36 14.5h72q21 0 35.5 -14.5t14.5 -34.5v-198l170 99q18 10 37.5 5t30.5 -23l35 -64q11 -18 6 -37t-23 -29l-171 -99l171 -99q18 -11 23 -29.5t-5 -36.5l-36 -65q-11 -17 -30.5 -22t-37.5 5
|
||||
l-170 99v-197q0 -21 -14.5 -35.5t-35.5 -14.5h-72q-22 0 -36 14.5t-14 34.5v198l-170 -99q-18 -11 -37.5 -5.5t-30.5 22.5l-37 65q-9 18 -4.5 36.5t22.5 29.5l172 99l-172 99q-18 10 -23 29z" />
|
||||
<glyph glyph-name="uni2753" unicode="❓" horiz-adv-x="610"
|
||||
d="M15 476q5 130 77 202t203 72q128 0 214 -62t86 -183q0 -68 -42 -125q-13 -19 -88 -78l-46 -32q-29 -22 -42 -43.5t-14 -60.5q0 -13 -15 -13h-129q-16 0 -16 12q2 52 6.5 79.5t20.5 45.5q17 20 49.5 46t56.5 42l23 16q61 46 61 102q0 44 -25 79q-25 36 -93 36
|
||||
q-67 0 -94 -44q-28 -45 -28 -91h-165zM292 54q45 -2 73.5 -31t27.5 -75t-31.5 -73t-75.5 -25q-44 2 -72.5 30.5t-27.5 73.5q1 46 31.5 74t74.5 26z" />
|
||||
<glyph glyph-name="uni2757" unicode="❗" horiz-adv-x="789"
|
||||
d="M0 329q0 82 31 153.5t84.5 125.5t125.5 85t154 31t153.5 -31t125 -85t84.5 -125.5t31 -153.5t-31 -153.5t-84.5 -125.5t-125 -85t-153.5 -31t-154 31t-125.5 85t-84.5 125.5t-31 153.5zM333 579l6 -365q2 -15 16 -15h80q14 0 14 15l8 365q1 6 -4 11q-4 4 -11 4h-95
|
||||
q-7 0 -10 -4q-4 -4 -4 -11zM336 66q0 -15 15 -15h89q5 0 10 4t5 11v86q0 6 -5 10.5t-10 4.5h-89q-15 0 -15 -15v-86z" />
|
||||
<glyph glyph-name="uni2795" unicode="➕" horiz-adv-x="870"
|
||||
d="M435 720q174 0 297 -123t123 -297t-123 -297t-297 -123t-297 123t-123 297t123 297t297 123zM486 249h202l-1 102h-201v201h-102v-201h-202v-102h202v-202h102v202z" />
|
||||
<glyph glyph-name="uni27A2" unicode="➢" horiz-adv-x="890"
|
||||
d="M863 717q6 -6 9 -12t2.5 -14.5t-2 -15.5t-7.5 -19.5t-12 -23.5l-16 -30l-18 -37q-54 -113 -147 -287l-158 -292l-65 -117l-54 380l-380 54q442 248 696 370l37 18l30 16q11 6 23.5 12t19.5 7.5t15.5 2t14.5 -2.5t12 -9zM772 619l-304 -279l28 -233z" />
|
||||
<glyph glyph-name="uni27A6" unicode="➦"
|
||||
d="M0 4q0 107 37 202t112 166t190.5 112.5t273.5 42.5v150q0 37 18 45t46 -18l252 -254q18 -17 18 -42q0 -24 -18 -42l-252 -254q-26 -26 -45 -18t-19 44v170q-135 -1 -237 -29t-172.5 -76t-110.5 -112.5t-51 -139.5q-2 -17 -19 -17h-1q-17 0 -19 17q-3 26 -3 53z" />
|
||||
<glyph glyph-name="uni27F2" unicode="⟲" horiz-adv-x="970"
|
||||
d="M546 710q169 0 289 -120t120 -290t-120 -290t-289 -120q-140 0 -251 87l70 75q83 -60 181 -60q127 0 217 90.5t90 217.5t-90 217t-217 90q-124 0 -213 -86.5t-93 -210.5h143l-184 -205l-184 205h123q4 167 123 283.5t285 116.5z" />
|
||||
<glyph glyph-name="uni27F3" unicode="⟳" horiz-adv-x="970"
|
||||
d="M424 710q166 0 285 -116.5t123 -283.5h123l-184 -205l-184 205h143q-4 124 -93 210.5t-213 86.5q-127 0 -217 -90t-90 -217t90 -217.5t217 -90.5q98 0 181 60l70 -75q-111 -87 -251 -87q-169 0 -289 120t-120 290t120 290t289 120z" />
|
||||
<glyph glyph-name="uni2B0C" unicode="⬌" horiz-adv-x="394"
|
||||
d="M0.5 131q4.5 12 28.5 12h100v372h-100q-24 0 -28.5 11t12.5 28l158 159q10 11 27 11q16 0 26 -11l158 -159q17 -16 12 -27.5t-29 -11.5h-100v-372h100q24 0 29 -11t-12 -28l-158 -159q-10 -11 -27 -11q-16 0 -26 11l-158 159q-17 15 -12.5 27z" />
|
||||
<glyph glyph-name="uni2B0D" unicode="⬍" horiz-adv-x="789"
|
||||
d="M0 310q0 16 11 26l158 159q16 16 27.5 11.5t11.5 -28.5v-101h373v101q0 24 11 28.5t28 -12.5l159 -158q10 -10 10 -26t-10 -26l-159 -159q-16 -17 -27.5 -12t-11.5 29v101h-373v-101q0 -24 -11 -28.5t-28 12.5l-158 158q-11 10 -11 26z" />
|
||||
<glyph glyph-name="uni2ECF" unicode="⻏" horiz-adv-x="789"
|
||||
d="M0 -30v102q0 15 10.5 25.5t25.5 10.5h718q14 0 24.5 -10.5t10.5 -25.5v-102q0 -15 -10.5 -25.5t-24.5 -10.5h-718q-15 0 -25.5 10.5t-10.5 25.5zM3 239q-9 23 8 39l358 359q11 11 25.5 11t25.5 -11l359 -359q17 -16 7 -39q-8 -22 -32 -22h-718q-24 0 -33 22z" />
|
||||
<glyph glyph-name="uniE700" unicode="" horiz-adv-x="1030"
|
||||
d="M634 78q89 -32 135 -65t46 -58v-105h-800v201q13 6 29.5 11.5t32 10t19.5 5.5q94 34 129.5 69t35.5 95q0 20 -10 31.5t-24 31.5t-21 58q-2 12 -9.5 17.5t-14.5 8t-14 17t-9 43.5q0 15 4.5 25.5t8.5 13.5l5 3q-9 50 -13 88q-2 21 5.5 47.5t27.5 55.5t63.5 48.5t104.5 19.5
|
||||
t104.5 -19.5t63.5 -48.5t27.5 -55.5t5.5 -47.5l-13 -88q18 -8 18 -42q-2 -29 -9 -43.5t-14 -17t-14.5 -8t-9.5 -17.5q-7 -38 -21 -58t-24 -31.5t-10 -31.5q0 -60 35.5 -95t129.5 -69zM865 350h150v-100h-150v-150h-100v150h-150v100h150v150h100v-150z" />
|
||||
<glyph glyph-name="uniE701" unicode="" horiz-adv-x="413"
|
||||
d="M0.5 423.5q4.5 13.5 26.5 17.5l251 35l111 228q10 20 25 20v-667l-223 -118q-22 -10 -33.5 -1.5t-6.5 31.5l43 248l-181 177q-17 16 -12.5 29.5z" />
|
||||
<glyph glyph-name="uniE704" unicode="" horiz-adv-x="950"
|
||||
d="M469 760q94 1 179.5 -34.5t148 -96t100 -145t38.5 -178.5t-34.5 -179.5t-96 -148t-145 -100t-178.5 -38.5t-179.5 34.5t-148 96t-100 145t-38.5 178.5t34.5 179.5t96 148t145 100t178.5 38.5zM535 84q1 31 -18 50t-50 20q-29 0 -48 -19t-20 -47q-1 -30 18 -48.5t50 -19.5
|
||||
q29 0 48 18t20 46zM475 486q67 0 67 -66q0 -14 -11 -31.5t-24 -27.5q-3 -1 -16 -10.5t-33 -25.5t-31 -28q-16 -19 -18 -87h107q0 21 5 34q6 16 28 34l28 19q30 23 43.5 36t26 37.5t12.5 55.5q0 74 -53.5 114t-135.5 40q-84 0 -128 -46.5t-48 -130.5h111v5q0 28 16 53
|
||||
q15 25 54 25z" />
|
||||
<glyph glyph-name="uniE705" unicode="" horiz-adv-x="950"
|
||||
d="M469 760q94 1 179.5 -34.5t148 -96t100 -145t38.5 -178.5t-34.5 -179.5t-96 -148t-145 -100t-178.5 -38.5t-179.5 34.5t-148 96t-100 145t-38.5 178.5t34.5 179.5t96 148t145 100t178.5 38.5zM520 607q-43 0 -65.5 -23.5t-22.5 -50.5q-1 -28 15.5 -43.5t48.5 -15.5
|
||||
q38 0 61 21.5t23 52.5q0 59 -60 59zM400 12q30 0 84.5 27t105.5 78l-18 24q-46 -36 -72 -36q-12 0 -3 38l42 159q26 96 -21 96q-31 0 -91 -28.5t-115 -74.5l16 -26q17 11 42.5 22t33.5 11t0 -34l-37 -151q-27 -105 33 -105z" />
|
||||
<glyph glyph-name="uniE708" unicode=""
|
||||
d="M0 -26v276q0 17 11.5 28.5t28.5 11.5h355q17 0 28.5 -11.5t11.5 -28.5v-276q0 -17 -11.5 -28.5t-28.5 -11.5h-355q-17 0 -28.5 11.5t-11.5 28.5zM0 408v277q0 16 11.5 27.5t28.5 11.5h355q17 0 28.5 -11.5t11.5 -27.5v-277q0 -17 -11.5 -28t-28.5 -11h-355
|
||||
q-17 0 -28.5 11t-11.5 28zM514 -26v276q0 17 11 28.5t28 11.5h355q16 0 27.5 -11.5t11.5 -28.5v-276q0 -17 -11.5 -28.5t-27.5 -11.5h-355q-17 0 -28 11.5t-11 28.5zM514 408v277q0 16 11 27.5t28 11.5h355q16 0 27.5 -11.5t11.5 -27.5v-277q0 -17 -11.5 -28t-27.5 -11h-355
|
||||
q-17 0 -28 11t-11 28z" />
|
||||
<glyph glyph-name="uniE70B" unicode=""
|
||||
d="M0 328.5q0 22.5 13 42.5q40 64 91.5 115t111 86.5t124.5 54t134 18.5q27 0 54 -4t53 -9l45 81q5 8 13 10q6 3 15 -1l68 -39q7 -5 10 -12.5t-1 -15.5l-398 -710q-4 -8 -12 -10q-2 -1 -5 -1t-10 2l-69 39q-8 4 -10 12t2 16l34 59q-75 34 -138.5 91t-111.5 134
|
||||
q-13 19 -13 41.5zM79 329q42 -67 98.5 -117.5t123.5 -81.5l30 54q-45 32 -71.5 81.5t-26.5 109.5q0 40 13 76.5t35 66.5q-60 -31 -111.5 -78.5t-90.5 -110.5zM316 375q0 -11 8.5 -20t21 -9t21 9t8.5 20q0 40 27 67.5t67 27.5q13 0 21.5 9t8.5 20q0 13 -8.5 21.5t-21.5 8.5
|
||||
q-31 0 -59.5 -12t-49 -32.5t-32.5 -49t-12 -60.5zM477 13l45 82q107 13 196 74.5t150 159.5q-55 87 -134 145l39 71q47 -34 88 -77t75 -97q11 -20 11 -42.5t-11 -41.5q-82 -131 -201.5 -202t-257.5 -72zM556 155l146 262q2 -10 3 -20t1 -22q0 -38 -11 -72t-31 -62.5
|
||||
t-48 -50.5t-60 -35z" />
|
||||
<glyph glyph-name="uniE70D" unicode="" horiz-adv-x="945"
|
||||
d="M0 434v229q0 25 18 43t43 18h228q26 0 57 -12.5t48 -30.5l338 -377q16 -18 16.5 -43.5t-16.5 -43.5l-266 -264q-18 -18 -43 -18.5t-43 18.5l-337 377q-17 19 -30 49t-13 55zM99 566q0 -24 17.5 -41.5t41.5 -17.5t41.5 17.5t17.5 41.5t-17.5 41.5t-41.5 17.5t-41.5 -17.5
|
||||
t-17.5 -41.5zM399 723h87q26 0 57 -13t48 -31l337 -376q17 -19 17.5 -44t-17.5 -43l-265 -264q-18 -18 -43 -18.5t-43 18.5l-6 7l258 258q18 18 17.5 43.5t-17.5 43.5l-336 377q-16 16 -43 28t-51 14z" />
|
||||
<glyph glyph-name="uniE711" unicode="" horiz-adv-x="1030"
|
||||
d="M776 443q99 0 169 -68.5t70 -165.5t-70 -165.5t-169 -68.5h-191v190h105l-175 230l-175 -230h105v-190h-249q-75 0 -128 52t-53 125t53 125t128 52q7 0 21 -2q-3 18 -3 38q0 108 78 184t188 76q89 0 159.5 -52t95.5 -133q19 3 41 3z" />
|
||||
<glyph glyph-name="uniE714" unicode="" horiz-adv-x="1350"
|
||||
d="M277 350l312 -311l-139 -139l-450 450l450 450l139 -139zM900 800l450 -450l-450 -450l-139 139l312 311l-312 311z" />
|
||||
<glyph glyph-name="uniE715" unicode=""
|
||||
d="M0 92v474q0 32 12.5 61t33.5 50t50 34t62 13h429q-3 -16 -3 -33v-26q-84 -11 -159 -40h-267q-24 0 -41.5 -17.5t-17.5 -41.5v-474q0 -24 17.5 -41.5t41.5 -17.5h553q24 0 41 17.5t17 41.5v57q9 5 17 11t16 14l66 65v-147q0 -33 -12.5 -61t-34 -50t-50 -34.5t-60.5 -12.5
|
||||
h-553q-33 0 -62 12.5t-50 34.5t-33.5 50t-12.5 61zM198 134v19q0 85 29 160.5t89 132t151 89t215 33.5v119q0 29 15 35.5t36 -14.5l201 -201q13 -13 13 -34q0 -20 -13 -32l-201 -202q-21 -21 -36 -14.5t-15 36.5v133q-106 0 -187 -22t-137 -60t-87.5 -89.5t-39.5 -110.5
|
||||
q-3 -14 -16 -14t-15 14q-2 12 -2 22z" />
|
||||
<glyph glyph-name="uniE716" unicode=""
|
||||
d="M0 -46v178q0 24 9.5 45.5t26 37.5t38 25.5t45.5 9.5h710q25 0 46.5 -9.5t37 -25.5t25 -37.5t9.5 -45.5v-178q0 -20 -20 -20h-907q-20 0 -20 20zM138 33q0 -8 5.5 -13.5t14.5 -5.5h631q9 0 14.5 5.5t5.5 13.5v20q0 9 -5.5 14.5t-14.5 5.5h-631q-9 0 -14.5 -5.5t-5.5 -14.5
|
||||
v-20zM158 309v376q0 16 11.5 27.5t28.5 11.5h335v-197q0 -25 17 -42.5t42 -17.5h197v-158h-631zM592 527v197l197 -197h-197z" />
|
||||
<glyph glyph-name="uniE718" unicode="" horiz-adv-x="830"
|
||||
d="M715 650q41 0 70.5 -29.5t29.5 -70.5v-350q0 -41 -29.5 -70.5t-70.5 -29.5h-200v-150l-200 150h-200q-41 0 -70.5 29.5t-29.5 70.5v350q0 41 29.5 70.5t70.5 29.5h600z" />
|
||||
<glyph glyph-name="uniE720" unicode=""
|
||||
d="M0 443q0 58 30.5 109t82.5 89t122 60.5t150 22.5t150 -22.5t122 -60.5t82 -89t30 -109q0 -59 -30 -110t-82 -89t-122 -60t-150 -22q-19 0 -36.5 1.5t-35.5 3.5q-36 -28 -77 -48t-89 -30q-21 -5 -42 -6q-12 -2 -17 11v1q-2 6 2 10l8 8q18 18 31.5 39t22.5 67
|
||||
q-70 38 -111 96t-41 128zM321 77l13 8q13 -2 26 -2h25q98 0 182.5 27.5t147 76t98 114.5t35.5 142q0 20 -3 41q48 -38 75 -86t27 -104q0 -69 -41 -126.5t-110 -96.5q8 -46 22 -66.5t31 -38.5q5 -5 8.5 -9.5t1.5 -9.5q-1 -6 -6.5 -10t-10.5 -3q-11 2 -21 3.5t-21 4.5
|
||||
q-93 20 -165 76q-18 -2 -36 -3.5t-37 -1.5q-69 0 -129.5 17t-111.5 47z" />
|
||||
<glyph glyph-name="uniE722" unicode="" horiz-adv-x="1030"
|
||||
d="M915 700q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-800q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h800zM915 0v600h-800v-600h800zM465 195v-90h-250v90h250zM465 345v-90h-250v90h250zM465 495v-90h-250v90h250zM810 175
|
||||
q2 0 3.5 -17.5t1.5 -35.5v-17h-250q0 70 5 70q10 2 24 7t37.5 22t23.5 37q0 16 -13.5 34.5t-27.5 45.5t-14 64q0 55 20.5 82.5t69.5 27.5t69.5 -27.5t20.5 -82.5q0 -37 -14 -64t-27.5 -45.5t-13.5 -34.5q0 -20 21.5 -36.5t42.5 -22.5z" />
|
||||
<glyph glyph-name="uniE724" unicode="" horiz-adv-x="557"
|
||||
d="M0 445q0 58 22 108.5t60 88t88.5 60t108.5 22.5t108.5 -22.5t88.5 -60t60 -88t22 -108.5q0 -42 -12.5 -79t-31.5 -71l-189 -327q-19 -34 -46 -34t-46 34l-189 327q-19 34 -31.5 71.5t-12.5 78.5zM141 444.5q0 -28.5 10.5 -53.5t29.5 -44t44.5 -29.5t53.5 -10.5t53.5 10.5
|
||||
t44 29.5t29.5 44t11 53.5t-11 54t-29.5 44t-44 29.5t-53.5 11t-53.5 -11t-44.5 -29.5t-29.5 -44t-10.5 -54z" />
|
||||
<glyph glyph-name="uniE729" unicode="" horiz-adv-x="682"
|
||||
d="M0 546.5q0 8.5 0.5 17t0.5 18.5q12 6 34.5 10t49.5 7.5t55 5.5t52 3q-3 17 -1.5 33.5t7.5 34.5q1 5 6 12.5t19.5 15.5t42.5 14t75 6t75 -6t42.5 -14t19.5 -16l6 -13q7 -18 8 -34t-1 -33q23 -1 51.5 -3t55.5 -5.5t49 -7.5t34 -10q1 -10 1 -18v-17v-17q0 -8 -1 -18
|
||||
q-10 -6 -32 -10.5t-48 -7.5t-53.5 -5t-50.5 -3t-39 -1.5t-17 -0.5l-100 -1h-14h-29q-20 0 -57 1q-2 0 -17.5 0.5t-38.5 1.5t-50.5 3t-53.5 5t-47.5 7.5t-32.5 10.5q0 10 -0.5 18t-0.5 16.5zM71 444q43 -6 88.5 -8.5t79.5 -3.5q9 -1 24 -1h78h77q15 0 24 1q35 1 80.5 3.5
|
||||
t89.5 8.5q-6 -80 -9 -163t-7 -163q-1 -20 -1.5 -44.5t-2 -47.5t-6.5 -42.5t-15 -30.5q-13 -11 -44.5 -15t-61.5 -4h-248q-31 0 -62 4t-43 15q-11 11 -15.5 30.5t-6.5 42.5t-2.5 47.5t-1.5 44.5q-4 80 -7.5 163t-7.5 163zM155 337q1 -15 1 -29t1 -23q0 -12 1 -20
|
||||
q2 -36 3.5 -72t3.5 -70q1 -9 1 -18v-21v-13q0 -6 0.5 -14.5t1.5 -20.5q1 -8 10 -14.5t15 -6.5q5 -1 10 -1t9 -1h11q5 0 5 20v301q0 9 -5.5 14.5t-14.5 5.5l-34 2q-9 0 -14 -5t-5 -14zM268.5 625.5q0.5 -6.5 2.5 -15.5q9 1 21 1h49l71 -1q1 9 1.5 15.5t-0.5 11.5
|
||||
q-12 4 -31.5 5.5t-40.5 1.5t-40.5 -1.5t-31.5 -5.5q-1 -5 -0.5 -11.5zM307 33q0 -8 5.5 -14t13.5 -6h30q8 0 14 6t6 14v300q0 9 -11.5 14t-15.5 5h-15q-3 0 -15 -5t-12 -14v-300zM455 33q0 -20 4 -20h12q3 1 13.5 1.5t15.5 0.5q5 1 9 7t5 14q0 12 0.5 20.5t0.5 14.5
|
||||
q0 7 1 13v21q0 9 1 18q2 34 3 69.5t3 71.5q0 9 1 21q1 9 1.5 23t1.5 29q0 9 -5 14t-14 5l-34 -2q-9 0 -14 -5.5t-5 -14.5v-301z" />
|
||||
<glyph glyph-name="uniE730" unicode="" horiz-adv-x="730"
|
||||
d="M615 750q41 0 70.5 -29.5t29.5 -70.5v-700q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v700q0 41 29.5 70.5t70.5 29.5h500zM615 -50v700h-500v-700h500z" />
|
||||
<glyph glyph-name="uniE736" unicode="" horiz-adv-x="730"
|
||||
d="M665 550q21 0 35.5 -14.5t14.5 -35.5v-600q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v150h-150q-21 0 -35.5 14.5t-14.5 35.5v600q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-150h150zM115 150h100v351q0 21 14 35t35 14h151v100
|
||||
h-300v-500zM615 -50v500h-300v-500h300z" />
|
||||
<glyph glyph-name="uniE73A" unicode="" horiz-adv-x="790"
|
||||
d="M15 680q155 0 295.5 -60t242.5 -162t162 -242.5t60 -295.5h-118q0 130 -51 249t-137 205t-205 137t-249 51v118zM15 443q142 0 262.5 -70t190.5 -190.5t70 -262.5h-118q0 167 -119 286t-286 119v118zM128 147q47 0 80.5 -33.5t33.5 -80.5t-33.5 -80t-80.5 -33t-80 33
|
||||
t-33 80t33 80.5t80 33.5z" />
|
||||
<glyph glyph-name="uniE73D" unicode="" horiz-adv-x="1006"
|
||||
d="M209 74q33 0 56 -23t23 -56t-23 -55t-56 -22q-31 0 -54.5 22t-23.5 55t22.5 56t55.5 23zM738.5 74q32.5 0 55 -23t22.5 -56t-22.5 -55t-55 -22t-55.5 22t-23 55t23 56t55.5 23zM261 662h565q26 0 40.5 -13.5t14.5 -36.5v-233q0 -20 -13 -34t-33 -16l-505 -57
|
||||
q-20 -2 -29 -16.5t0 -28t31 -13.5h479q27 0 37.5 -21t0 -41t-37.5 -20h-489q-52 0 -84 33t-27 77t47 76l43 32l-155 339h-76q-28 0 -39 22.5t0 45t39 22.5h89q31 0 45 -12.5t26 -41.5zM606 592h-130l26 -84h124zM294 592l38 -84h127l-28 84h-137zM402 351l100 10l-32 101
|
||||
h-119zM546 366l113 12l-25 84h-119zM812 395v67h-132l20 -78zM812 508v84h-161l19 -84h142z" />
|
||||
<glyph glyph-name="uniE744" unicode="" horiz-adv-x="789"
|
||||
d="M0 -22v269q0 27 13.5 33t33.5 -13l86 -85l141 141q6 6 15.5 6t16.5 -6l82 -82q7 -8 7 -17.5t-7 -15.5l-141 -141l86 -85q20 -20 13.5 -34t-33.5 -14h-268q-19 0 -31 13q-14 14 -14 31zM395 434.5q0 9.5 6 15.5l141 141l-85 85q-20 20 -14 34t34 14h267q19 0 32 -13
|
||||
q13 -14 13 -32v-268q0 -27 -13.5 -33t-33.5 13l-85 85l-141 -140q-7 -7 -16 -7t-17 7l-82 81q-6 8 -6 17.5z" />
|
||||
<glyph glyph-name="uniE746" unicode="" horiz-adv-x="789"
|
||||
d="M0 38.5q0 9.5 6 16.5l141 140l-85 86q-20 20 -13.5 33.5t33.5 13.5h267q20 0 32 -13q14 -13 14 -31v-269q0 -27 -14 -33t-34 14l-85 86l-141 -141q-6 -8 -15.5 -8t-17.5 8l-82 82q-6 6 -6 15.5zM395 373v269q0 27 13.5 33t33.5 -14l85 -86l141 141q7 8 16.5 8t16.5 -8
|
||||
l82 -82q6 -6 6 -15.5t-6 -16.5l-141 -140l85 -86q20 -20 14 -33.5t-34 -13.5h-267q-21 0 -32 13q-13 13 -13 31z" />
|
||||
<glyph glyph-name="uniE750" unicode="" horiz-adv-x="789"
|
||||
d="M0 394.5q0 68.5 26 128.5t70.5 104.5t104.5 70.5t128 26t128 -26t104.5 -70.5t70.5 -104.5t26 -129q0 -50 -14.5 -95.5t-40.5 -85.5l177 -176q9 -10 9 -24t-9 -23l-46 -46q-10 -10 -23.5 -10t-23.5 10l-176 177q-40 -27 -85.5 -41t-96.5 -14q-68 0 -128 25.5t-104.5 70.5
|
||||
t-70.5 104.5t-26 128zM132 394q0 -41 15 -76.5t42 -62.5t63 -42.5t77.5 -15.5t77 15.5t62.5 42.5t42 62.5t15 76.5t-15 77t-42 63t-62.5 42.5t-77 15.5t-77.5 -15.5t-63 -42.5t-42 -63t-15 -77zM198 378v33q0 7 4.5 12t11.5 5h82v82q0 6 4.5 11t12.5 5h32q8 0 12.5 -5
|
||||
t4.5 -11v-82h82q17 0 17 -17v-33q0 -16 -17 -16h-82v-83q0 -16 -17 -16h-32q-17 0 -17 16v83h-82q-7 0 -11.5 4.5t-4.5 11.5z" />
|
||||
<glyph glyph-name="uniE751" unicode="" horiz-adv-x="789"
|
||||
d="M0 394.5q0 68.5 26 128.5t70.5 104.5t104.5 70.5t128 26t128 -26t104.5 -70.5t70.5 -104.5t26 -129q0 -50 -14.5 -95.5t-40.5 -85.5l177 -176q9 -10 9 -24t-9 -23l-46 -46q-10 -10 -23.5 -10t-23.5 10l-176 177q-40 -27 -85.5 -41t-96.5 -14q-68 0 -128 25.5t-104.5 70.5
|
||||
t-70.5 104.5t-26 128zM132 394q0 -41 15 -76.5t42 -62.5t63 -42.5t77.5 -15.5t77 15.5t62.5 42.5t42 62.5t15 76.5t-15 77t-42 63t-62.5 42.5t-77 15.5t-77.5 -15.5t-63 -42.5t-42 -63t-15 -77zM198 378v33q0 7 4.5 12t11.5 5h230q17 0 17 -17v-33q0 -16 -17 -16h-230
|
||||
q-7 0 -11.5 4.5t-4.5 11.5z" />
|
||||
<glyph glyph-name="uniE75C" unicode="" horiz-adv-x="789"
|
||||
d="M0 406.5q0 19.5 15 34.5l51 52q15 15 35 15t35 -15l261 -262l257 262q14 15 34 15t35 -15l52 -52q14 -15 15 -34.5t-14 -34.5l-347 -346q-14 -15 -34 -15t-35 15l-345 346q-15 15 -15 34.5z" />
|
||||
<glyph glyph-name="uniE75D" unicode="" horiz-adv-x="495"
|
||||
d="M0 327.5q0 19.5 15 34.5l345 346q15 15 35 15t34 -15l52 -52q15 -14 15 -34t-15 -35l-261 -262l261 -257q15 -15 15 -34.5t-15 -34.5l-52 -52q-14 -15 -34 -15t-35 15l-345 346q-15 15 -15 34.5z" />
|
||||
<glyph glyph-name="uniE75E" unicode="" horiz-adv-x="495"
|
||||
d="M0 34q0 20 15 35l261 261l-261 258q-15 15 -15 34.5t15 33.5l51 53q15 15 35 15t35 -15l345 -346q15 -15 15 -35t-15 -35l-345 -345q-15 -15 -35 -15t-35 15l-51 51q-15 15 -15 35z" />
|
||||
<glyph glyph-name="uniE75F" unicode="" horiz-adv-x="789"
|
||||
d="M-0.5 116.5q-0.5 19.5 14.5 34.5l346 346q15 15 35 15t34 -15l346 -346q14 -15 14 -34.5t-14 -33.5l-52 -53q-15 -15 -35 -15t-34 15l-261 262l-257 -262q-15 -15 -35 -15t-35 15l-51 53q-15 14 -15.5 33.5z" />
|
||||
<glyph glyph-name="uniE762" unicode=""
|
||||
d="M0 -34v726q0 20 18 30q20 7 33 -8l335 -362q9 -9 9 -23t-9 -23l-335 -362q-9 -10 -22 -10q-3 0 -11 2q-18 9 -18 30zM395 -34v726q0 20 18 30q20 7 32 -8l336 -362q8 -9 8 -23t-8 -23l-336 -362q-8 -10 -21 -10q-3 0 -11 2q-18 9 -18 30zM789 -27q0 -16 12 -27.5
|
||||
t27 -11.5h79q17 0 28.5 11.5t11.5 27.5v711q0 17 -11.5 28.5t-28.5 11.5h-79q-15 0 -27 -11.5t-12 -28.5v-711z" />
|
||||
<glyph glyph-name="uniE763" unicode=""
|
||||
d="M0 -27q0 -16 11.5 -27.5t27.5 -11.5h79q17 0 28.5 11.5t11.5 27.5v711q0 17 -11.5 28.5t-28.5 11.5h-79q-16 0 -27.5 -11.5t-11.5 -28.5v-711zM158 329q0 14 8 22l336 363q7 10 21 10q3 0 12 -2q18 -10 18 -31v-725q0 -21 -18 -30q-20 -7 -33 8l-336 362q-8 9 -8 23z
|
||||
M553 329q0 14 8 22l336 363q7 10 21 10q3 0 11 -2q18 -10 18 -31v-725q0 -21 -18 -30q-20 -7 -32 8l-336 362q-8 9 -8 23z" />
|
||||
<glyph glyph-name="uniE782" unicode="" horiz-adv-x="1228"
|
||||
d="M256 38.5q0 41.5 30.5 72t71.5 30.5h513q40 0 71 -30.5t31 -72t-31 -72t-71 -30.5h-513q-41 0 -71.5 30.5t-30.5 72zM256 294.5q0 40.5 30.5 71.5t71.5 31h513q40 0 71 -31t31 -71.5t-31 -71.5t-71 -31h-513q-41 0 -71.5 31t-30.5 71.5zM256 550q0 41 30.5 72t71.5 31
|
||||
h513q40 0 71 -31t31 -72t-31 -71.5t-71 -30.5h-513q-41 0 -71.5 30.5t-30.5 71.5z" />
|
||||
<glyph glyph-name="uniE78E" unicode="" horiz-adv-x="750"
|
||||
d="M0 309q0 94 43 175.5t121 134.5q7 4 15 4q7 -1 13 -9l44 -65q10 -17 -5 -27q-54 -37 -83 -93t-29 -120q0 -53 20 -99.5t54.5 -81.5t81.5 -55t100 -20t100 20t82 55t55 81.5t20 99.5q0 64 -29.5 120t-83.5 93q-6 3 -8 12q-2 7 3 15l45 65q5 7 12.5 8.5t14.5 -3.5
|
||||
q77 -53 121 -134.5t44 -175.5q0 -78 -29.5 -146t-81 -119t-119.5 -80.5t-146 -29.5t-146 29.5t-119 80.5t-80.5 119t-29.5 146zM316 349v355q0 20 20 20h79q8 0 14 -6t6 -14v-355q0 -8 -6 -14t-14 -6h-79q-20 0 -20 20z" />
|
||||
<glyph glyph-name="uniE792" unicode=""
|
||||
d="M0 -66v790h79v-790h-79zM117 -66v790h19v-790h-19zM183 -66v790h60v-790h-60zM280 -66v790h40v-790h-40zM367 -66v790h39v-790h-39zM464 -66v790h19v-790h-19zM521 -66v790h79v-790h-79zM627 -66v790h40v-790h-40zM724 -66v790h20v-790h-20zM812 -66v790h19v-790h-19z
|
||||
M868 -66v790h79v-790h-79z" />
|
||||
<glyph glyph-name="uniE794" unicode="" horiz-adv-x="558"
|
||||
d="M0 214q0 41 11.5 79t32.5 71q11 17 35 49.5t53 76t56.5 95t44.5 107.5q5 18 18 26t28 6q16 2 29 -6t18 -26q17 -56 44.5 -107.5t56.5 -95t53 -76t35 -49.5q21 -33 32.5 -71t11.5 -79q0 -58 -22 -109t-60 -89t-89 -60t-109 -22t-108.5 22t-88.5 60t-60 89t-22 109z
|
||||
M122 152q0 -29 20.5 -49t50.5 -20q28 0 48.5 20t20.5 49q0 19 -10 38q-4 4 -10 12t-13 19t-14 24t-11 27q-4 10 -11 7q-10 3 -12 -7q-4 -14 -11.5 -27t-14.5 -24t-13 -19t-8 -12q-12 -18 -12 -38z" />
|
||||
<glyph glyph-name="uniE7A2" unicode="" horiz-adv-x="917"
|
||||
d="M842 355h-331v335q138 0 234.5 -97t96.5 -234v-4zM406 250h333v4q0 -138 -97.5 -235t-235 -97t-234.5 97t-97 234.5t97 235t234 97.5v-336z" />
|
||||
<glyph glyph-name="uniE7AC" unicode="" horiz-adv-x="1224"
|
||||
d="M144 329l285 122v86l-285 121v-89l188 -76l-188 -75v-89zM470 260h286v69h-286v-69zM1224 800v-900h-1224v900h1224z" />
|
||||
<glyph glyph-name="uniE7B5" unicode="" horiz-adv-x="1021"
|
||||
d="M475 -139v115l-150 -57l-15 26l165 101v58h-317q-19 0 -32.5 13t-13.5 32q0 17 12 29.5t30 15.5v414q-18 1 -30 13.5t-12 29.5q0 19 13.5 32t32.5 13h326l18 49h12l18 -49h332q18 0 31.5 -13t13.5 -32t-13.5 -32t-31.5 -13v-412q18 0 31.5 -13t13.5 -32t-13.5 -32
|
||||
t-31.5 -13h-328v-58l166 -101l-14 -26l-152 57v-115q0 -13 -9 -22.5t-22 -9.5q-14 0 -22 9.5t-8 22.5zM806 198v404h-595v-404h595z" />
|
||||
<glyph glyph-name="uniE800" unicode="" horiz-adv-x="828"
|
||||
d="M0.5 423.5q4.5 13.5 26.5 17.5l251 35l111 228q10 20 24.5 20t24.5 -20l113 -228l249 -35q23 -4 27.5 -17.5t-11.5 -29.5l-181 -177l42 -248q3 -17 -2.5 -26t-16.5 -9q-7 0 -20 5l-224 118l-223 -118q-13 -5 -20 -5q-12 0 -17.5 9t-2.5 26l43 248l-181 177
|
||||
q-17 16 -12.5 29.5zM143 377l105 -103l31 -28l-7 -41l-26 -147l131 70l37 19l37 -19l130 -70l-24 147l-8 41l30 28l106 103l-146 21l-41 7l-19 37l-65 132l-66 -132l-17 -37l-42 -7z" />
|
||||
<glyph glyph-name="uniE801" unicode="" horiz-adv-x="828"
|
||||
d="M0.5 423.5q4.5 13.5 26.5 17.5l251 36l111 227q11 21 25 21t24 -21l113 -227l249 -36q23 -4 27.5 -17.5t-11.5 -29.5l-181 -177l42 -248q4 -23 -7.5 -31.5t-31.5 3.5l-224 116l-223 -116q-22 -12 -33.5 -3.5t-6.5 31.5l43 248l-181 177q-17 16 -12.5 29.5z" />
|
||||
<glyph glyph-name="uniE802" unicode=""
|
||||
d="M0 377q0 72 37.5 135t101.5 110t150.5 74.5t184.5 27.5t184 -27.5t150.5 -74.5t101.5 -110t37 -134.5t-37 -135t-101.5 -110t-150.5 -74t-184 -27.5q-47 0 -89 6q-44 -33 -95 -57.5t-109 -36.5l-25 -5l-27 -4q-16 -2 -21 15v1q-2 7 4 12l9 10l21 23q10 11 17.5 25.5
|
||||
t14.5 34t12 48.5q-85 47 -135.5 118t-50.5 156z" />
|
||||
<glyph glyph-name="uniE803" unicode=""
|
||||
d="M0 63v79q0 20 20 20h113q26 0 50.5 16t48 43t46.5 61l46 72l60 90q30 45 64.5 79.5t75.5 56.5t93 22h104v94q0 21 12.5 25.5t30.5 -10.5l172 -142q11 -10 11 -24t-11 -23l-172 -144q-18 -14 -30.5 -10t-12.5 25v91h-104q-28 0 -52 -16.5t-47.5 -43.5t-47.5 -62l-46 -71
|
||||
q-28 -46 -58 -90.5t-65 -79t-76.5 -56t-91.5 -21.5h-113q-8 0 -14 5t-6 14zM0 504v79q0 8 6 14t14 6h113q54 0 98 -25t81 -65q-19 -26 -36 -52l-33 -50q-26 33 -53 53t-57 20h-113q-8 0 -14 5.5t-6 14.5zM439 134l35 51l32 51q27 -32 53.5 -52t57.5 -20h104v99q0 21 12.5 25
|
||||
t30.5 -11l172 -143q11 -9 11 -23q0 -15 -11 -24l-172 -142q-18 -15 -30.5 -11t-12.5 25v87h-104q-56 0 -98.5 24t-79.5 64z" />
|
||||
<glyph glyph-name="uniE804" unicode="" horiz-adv-x="789"
|
||||
d="M0 -34v726q0 20 18 30q20 7 33 -8l335 -362q9 -9 9 -23t-9 -23l-335 -362q-9 -10 -22 -10q-3 0 -11 2q-18 9 -18 30zM395 -34v726q0 20 18 30q20 7 32 -8l336 -362q8 -9 8 -23t-8 -23l-336 -362q-8 -10 -21 -10q-3 0 -11 2q-18 9 -18 30z" />
|
||||
<glyph glyph-name="uniE805" unicode="" horiz-adv-x="552"
|
||||
d="M0 -27q0 -16 11.5 -27.5t27.5 -11.5h79q17 0 28.5 11.5t11.5 27.5v711q0 17 -11.5 28.5t-28.5 11.5h-79q-16 0 -27.5 -11.5t-11.5 -28.5v-711zM158 329q0 14 8 22l336 363q7 10 21 10q3 0 12 -2q18 -10 18 -31v-725q0 -21 -18 -30q-20 -7 -33 8l-336 362q-8 9 -8 23z" />
|
||||
<glyph glyph-name="uniE806" unicode="" horiz-adv-x="552"
|
||||
d="M0 -34v726q0 20 18 30q20 7 33 -8l335 -362q9 -9 9 -23t-9 -23l-335 -362q-9 -10 -22 -10q-3 0 -11 2q-18 9 -18 30zM395 -27q0 -16 11.5 -27.5t27.5 -11.5h79q16 0 28 11.5t12 27.5v711q0 17 -12 28.5t-28 11.5h-79q-16 0 -27.5 -11.5t-11.5 -28.5v-711z" />
|
||||
<glyph glyph-name="uniE807" unicode="" horiz-adv-x="789"
|
||||
d="M0 -30v718q0 15 10.5 25.5t25.5 10.5h718q14 0 24.5 -10.5t10.5 -25.5v-718q0 -15 -10.5 -25.5t-24.5 -10.5h-718q-15 0 -25.5 10.5t-10.5 25.5z" />
|
||||
<glyph glyph-name="uniE808" unicode="" horiz-adv-x="789"
|
||||
d="M0 -30v718q0 15 10.5 25.5t25.5 10.5h251q15 0 25.5 -10.5t10.5 -25.5v-718q0 -15 -10.5 -25.5t-25.5 -10.5h-251q-15 0 -25.5 10.5t-10.5 25.5zM466 -30v718q0 15 10.5 25.5t25.5 10.5h252q14 0 24.5 -10.5t10.5 -25.5v-718q0 -15 -10.5 -25.5t-24.5 -10.5h-252
|
||||
q-15 0 -25.5 10.5t-10.5 25.5z" />
|
||||
<glyph glyph-name="uniE809" unicode="" horiz-adv-x="693"
|
||||
d="M0 -30v718q0 20 18 30q19 12 36 0l622 -357q18 -13 18 -32t-18 -32l-622 -358q-9 -5 -18.5 -5t-17.5 5q-18 11 -18 31z" />
|
||||
<glyph glyph-name="uniE80A" unicode="" horiz-adv-x="789"
|
||||
d="M0 433v230q0 25 18 43t43 18h230q25 0 55.5 -13t48.5 -30l377 -377q17 -18 17 -43.5t-17 -43.5l-266 -265q-18 -18 -43 -18t-43 18l-377 377q-18 18 -30.5 48.5t-12.5 55.5zM99 566q0 -24 17.5 -41.5t41.5 -17.5t41.5 17.5t17.5 41.5t-17.5 41.5t-41.5 17.5t-41.5 -17.5
|
||||
t-17.5 -41.5z" />
|
||||
<glyph glyph-name="uniE80B" unicode=""
|
||||
d="M0 -26v139q0 17 11.5 28t28.5 11h153q15 0 27 -11t12 -28v-139q0 -17 -12 -28.5t-27 -11.5h-153q-17 0 -28.5 11.5t-11.5 28.5zM0 271v116q0 17 11.5 28t28.5 11h153q15 0 27 -11t12 -28v-116q0 -17 -12 -28.5t-27 -11.5h-153q-17 0 -28.5 11.5t-11.5 28.5zM0 545v140
|
||||
q0 16 11.5 27.5t28.5 11.5h153q15 0 27 -11.5t12 -27.5v-140q0 -17 -12 -28.5t-27 -11.5h-153q-17 0 -28.5 11.5t-11.5 28.5zM311 -26v139q0 17 11.5 28t28.5 11h557q16 0 27.5 -11t11.5 -28v-139q0 -17 -11.5 -28.5t-27.5 -11.5h-557q-17 0 -28.5 11.5t-11.5 28.5zM311 271
|
||||
v116q0 17 11.5 28t28.5 11h557q16 0 27.5 -11t11.5 -28v-116q0 -17 -11.5 -28.5t-27.5 -11.5h-557q-17 0 -28.5 11.5t-11.5 28.5zM311 545v140q0 16 11.5 27.5t28.5 11.5h557q16 0 27.5 -11.5t11.5 -27.5v-140q0 -17 -11.5 -28.5t-27.5 -11.5h-557q-17 0 -28.5 11.5
|
||||
t-11.5 28.5z" />
|
||||
<glyph glyph-name="uniE80C" unicode=""
|
||||
d="M0 -26v139q0 17 11.5 28t28.5 11h184q16 0 27.5 -11t11.5 -28v-139q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5zM0 271v116q0 17 11.5 28t28.5 11h184q16 0 27.5 -11t11.5 -28v-116q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5
|
||||
t-11.5 28.5zM0 545v140q0 16 11.5 27.5t28.5 11.5h184q16 0 27.5 -11.5t11.5 -27.5v-140q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5zM342 -26v139q0 17 11.5 28t28.5 11h184q16 0 27.5 -11t11.5 -28v-139q0 -17 -11.5 -28.5t-27.5 -11.5h-184
|
||||
q-17 0 -28.5 11.5t-11.5 28.5zM342 271v116q0 17 11.5 28t28.5 11h184q16 0 27.5 -11t11.5 -28v-116q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5zM342 545v140q0 16 11.5 27.5t28.5 11.5h184q16 0 27.5 -11.5t11.5 -27.5v-140q0 -17 -11.5 -28.5
|
||||
t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5zM684 -26v139q0 17 11.5 28t28.5 11h184q16 0 27.5 -11t11.5 -28v-139q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5zM684 271v116q0 17 11.5 28t28.5 11h184q16 0 27.5 -11t11.5 -28v-116
|
||||
q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5zM684 545v140q0 16 11.5 27.5t28.5 11.5h184q16 0 27.5 -11.5t11.5 -27.5v-140q0 -17 -11.5 -28.5t-27.5 -11.5h-184q-17 0 -28.5 11.5t-11.5 28.5z" />
|
||||
<glyph glyph-name="uniE80D" unicode="" horiz-adv-x="789"
|
||||
d="M0 33q0 20 15 35l261 262l-261 257q-15 15 -15 35t15 34l51 52q15 15 35 15t35 -15l259 -259l259 259q14 15 34 15t35 -15l52 -52q14 -14 14 -34t-14 -35l-261 -262l261 -257q14 -15 14 -34.5t-14 -34.5l-52 -52q-15 -15 -35 -15t-34 15l-259 260l-260 -260
|
||||
q-15 -15 -34.5 -15t-34.5 15l-51 51q-15 15 -15 35z" />
|
||||
<glyph glyph-name="uniE80E" unicode=""
|
||||
d="M1 339q3 12 14 20l433 356q12 9 25.5 9t25.5 -9l133 -109v61q0 20 20 20h117q20 0 20 -20v-190l144 -118q10 -8 13 -20t-1 -24q-10 -25 -38 -25h-79v-316q0 -17 -11 -28.5t-28 -11.5h-216v237h-198v-237h-217q-17 0 -28.5 11.5t-11.5 28.5v316h-79q-26 0 -37 25
|
||||
q-4 12 -1 24z" />
|
||||
<glyph glyph-name="uniE80F" unicode=""
|
||||
d="M0 305.5q0 22.5 13 42.5q40 64 91.5 115t111 86.5t124.5 54t134 18.5t134.5 -18.5t124.5 -53.5t110.5 -86t92.5 -116q11 -20 11 -42.5t-11 -41.5q-41 -66 -92.5 -117t-110.5 -85.5t-124.5 -53t-134.5 -18.5t-134 18.5t-124.5 53.5t-111 86t-91.5 116q-13 19 -13 41.5z
|
||||
M79 306q34 -54 77 -98t93.5 -74.5t107 -47.5t117.5 -17t117 17t106.5 47.5t94 74.5t76.5 98q-41 66 -96 116t-121 81q26 -31 40.5 -69t14.5 -82q0 -49 -18.5 -92t-51 -75.5t-75.5 -51t-92 -18.5t-91.5 18.5t-75.5 51t-51 75.5t-18 92q0 40 13 76.5t35 65.5
|
||||
q-60 -30 -111.5 -78t-90.5 -110zM316 352q0 -11 8.5 -20t21 -9t21 9t8.5 20q0 40 27 67.5t67 27.5q13 0 21.5 9t8.5 20q0 13 -8.5 21.5t-21.5 8.5q-31 0 -59.5 -12t-49 -32.5t-32.5 -49t-12 -60.5z" />
|
||||
<glyph glyph-name="uniE810" unicode=""
|
||||
d="M0 -7v672q0 24 17.5 41.5t41.5 17.5h356q24 0 41.5 -17.5t17.5 -41.5t17 -41.5t42 -17.5h355q25 0 42 -18t17 -42v-553q0 -24 -17 -41.5t-42 -17.5h-829q-24 0 -41.5 17.5t-17.5 41.5z" />
|
||||
<glyph glyph-name="uniE811" unicode="" horiz-adv-x="530"
|
||||
d="M265 700q103 0 176.5 -73t73.5 -177q0 -67 -25.5 -148t-62.5 -149.5t-74 -127t-63 -91.5l-25 -34l-27 35q-17 22 -60 89t-76 130t-60 146t-27 150q0 104 73.5 177t176.5 73zM265 312q56 0 95.5 39.5t39.5 95.5t-39.5 95.5t-95.5 39.5t-95.5 -39.5t-39.5 -95.5t39.5 -95.5
|
||||
t95.5 -39.5z" />
|
||||
<glyph glyph-name="uniE812" unicode="" horiz-adv-x="1030"
|
||||
d="M871 466h-100l-124 151l-214 -151h-180q-53 0 -90.5 -38t-37.5 -92v-159l-107 297q-6 16 0.5 31t21.5 20l680 248q15 5 29 -2t20 -23zM977 376q16 0 27 -11.5t11 -28.5v-471q0 -17 -11 -28.5t-27 -11.5h-724q-16 0 -27 11.5t-11 28.5v471q0 17 11 28.5t27 11.5h724z
|
||||
M922 -75v161l-73 161l-167 -60l-129 -133l-139 172l-92 -215v-86h600z" />
|
||||
<glyph glyph-name="uniE813" unicode=""
|
||||
d="M0 665v39q0 20 20 20h119q8 0 19 -2.5t19 -4.5q3 -2 7 -7t8 -12t6.5 -14t3.5 -11l14 -64h692q18 0 29.5 -13.5t8.5 -30.5l-55 -297q-4 -13 -14.5 -22t-24.5 -9h-557l18 -86q2 -8 8.5 -13.5t15.5 -5.5h440q8 0 14 -5.5t6 -14.5v-40q0 -8 -6 -13.5t-14 -5.5h-84h-335h-54
|
||||
q-8 0 -18.5 1.5t-18.5 4.5q-3 2 -7 7.5t-8 12.5t-6.5 13.5t-3.5 11.5l-110 522q-3 8 -9.5 13.5t-15.5 5.5h-87q-20 0 -20 20zM299 -7q0 24 17.5 42t41.5 18q25 0 42.5 -18t17.5 -42t-17.5 -41.5t-42.5 -17.5q-24 0 -41.5 17.5t-17.5 41.5zM634 -7q0 24 17 42t41.5 18t42 -18
|
||||
t17.5 -42t-17.5 -41.5t-42 -17.5t-41.5 17.5t-17 41.5z" />
|
||||
<glyph glyph-name="uniE814" unicode="" horiz-adv-x="1228"
|
||||
d="M31 151q0 46 82 97q66 36 143 46v51q-77 77 -77 190q0 194 154 194q77 0 97 -20q26 -20 36 -92q-97 -66 -98 -246q0 -159 67 -230q-26 0 -71 -31l-36 -20l-11 -10q-15 -11 -25 -21l-26 -26q-25 0 -138 31q-97 31 -97 87zM281 -43.5q0 40.5 93 97.5q87 46 158 51v56
|
||||
q-82 77 -81 205q0 210 163.5 210t163.5 -210q0 -128 -81 -205v-56q71 -6 158 -51.5t87 -96.5q0 -41 -71 -62q-77 -21 -149 -31q-46 -5 -107.5 -5t-107.5 5q-71 10 -148.5 31t-77.5 61.5zM763 617q10 72 36 92q20 20 97 20q153 0 153 -194q0 -113 -76 -190v-51
|
||||
q76 -10 143 -46q87 -46 87 -97t-102 -87q-113 -31 -139 -31l-25 26q-10 10 -26 21l-10 10q-20 10 -36 20q-46 31 -71 31q66 71 66 230q0 180 -97 246z" />
|
||||
<glyph glyph-name="uniE815" unicode="" horiz-adv-x="1228"
|
||||
d="M358 341l384 414l-128 -358l257 -149l-384 -414l127 358z" />
|
||||
<glyph glyph-name="uniE817" unicode="" horiz-adv-x="961"
|
||||
d="M481 722q169 0 287 -117t118 -286q0 -170 -117.5 -288.5t-287.5 -118.5q-169 0 -287.5 118.5t-118.5 288.5q0 167 118.5 285t287.5 118zM504 -11q67 0 116 169q-58 -6 -116 -8v-161zM375 70q34 -81 81 -81v161q-58 2 -115 8q11 -48 34 -88zM152 272q34 -40 133 -57
|
||||
q-7 46 -7 100q0 33 3 69q-82 16 -117 31q-14 -47 -14 -96q0 -33 2 -47zM322 315q0 -57 8 -109q60 -10 126 -12v176q-76 2 -132 9q0 -11 -1 -32t-1 -32zM454 646q-43 -16 -76 -77t-48 -148q61 -5 126 -7v232h-2zM536 629q-26 17 -32 17v-232q62 1 127 7q-26 145 -95 208z
|
||||
M504 194q64 1 126 11q8 40 8 110q0 41 -2 63q-64 -7 -132 -8v-176zM680 384q2 -24 2 -69q0 -49 -6 -101q90 15 112.5 36.5t22.5 68.5q0 50 -15 95q-57 -24 -116 -30zM779 458q-57 123 -179 167q54 -70 74 -199q84 15 105 32zM248 550q-40 -36 -66 -91q6 -5 104 -31
|
||||
q22 124 75 196q-80 -38 -113 -74zM248 85q40 -44 106 -71q-40 56 -60 151q-84 19 -125 41q31 -77 79 -121zM714 85q48 45 75 116q-47 -21 -121 -37q-23 -96 -62 -150q68 28 108 71z" />
|
||||
<glyph glyph-name="uniF301" unicode="" horiz-adv-x="978"
|
||||
d="M188 800h602q73 0 123.5 -51t50.5 -123v-602q0 -72 -50.5 -123t-123.5 -51h-602q-72 0 -123 51t-51 123v602q0 72 51 123t123 51zM515 -150h141v371h139l6 134h-145v97q0 32 10.5 46t45.5 14l85 -1l4 126q-41 4 -95 4q-95 0 -143 -49t-48 -123v-114h-99v-134h99v-371z
|
||||
" />
|
||||
<glyph glyph-name="uniF303" unicode="" horiz-adv-x="1332"
|
||||
d="M1318 385q-35 -5 -77.5 -0.5t-69.5 15.5q53 4 90.5 27t51.5 57q-22 -14 -69.5 -21t-84.5 4q-1 4 -3 14.5t-4 16.5q-26 97 -105.5 161.5t-170.5 54.5l29 11q2 1 14.5 4t24 6.5t23.5 9t17.5 12t1.5 14.5q-2 7 -14.5 7.5t-29.5 -4t-32.5 -9.5t-31 -11t-18.5 -7q64 24 69 53
|
||||
q-59 -8 -103 -49q17 19 20 39q-68 -43 -115 -122.5t-89 -192.5q-62 59 -99 79q-136 73 -335 148q-4 -42 23.5 -87.5t88.5 -77.5q-27 4 -81 -9q9 -48 43 -81t104 -49q-61 -4 -90 -35q19 -37 59.5 -61.5t101.5 -18.5q-35 -15 -46.5 -36.5t-2 -39t34.5 -28.5t55 -6
|
||||
q-68 -71 -158.5 -94.5t-177 -1.5t-148.5 82q79 -108 191.5 -172.5t229 -80t237 6.5t219.5 77.5t172.5 142.5t99.5 193q101 -1 155 59z" />
|
||||
<glyph glyph-name="uniF308" unicode="" horiz-adv-x="1132"
|
||||
d="M456 224q0 51 -22 87.5t-52.5 36.5t-52 -36.5t-21.5 -87.5t21.5 -87t52.5 -36t52.5 36t21.5 87zM856 224q0 51 -22 87.5t-52.5 36.5t-53 -36.5t-22.5 -87.5t22 -87t53.5 -36t53 36t21.5 87zM933 238q0 -37 -4.5 -68.5t-14.5 -55t-21 -42.5t-29.5 -32.5t-35 -23.5
|
||||
t-42.5 -16t-46.5 -10t-52 -6t-54.5 -2.5t-58 -0.5q-65 0 -114 5.5t-97 21.5t-78 43.5t-49 74.5t-19 112q0 78 61 142q23 24 58.5 33.5t65.5 8.5t84.5 -5.5t87.5 -4.5t87 4.5t85 5.5t66.5 -8.5t57.5 -32.5q62 -67 62 -143zM724 -99q156 15 239.5 64.5t120.5 136.5q6 12 19 59
|
||||
q16 52 16 174q0 146 -102 248q31 101 -15 216q-3 1 -9 2t-27 -1t-45.5 -9t-65.5 -27t-85 -50q-91 23 -198 23q-115 0 -218 -28q-47 32 -89 53t-68.5 28t-46.5 10t-28 1l-9 -2q-21 -54 -24 -108.5t2.5 -79.5t12.5 -43q-90 -98 -90 -233q0 -115 27 -193q2 -8 22 -48
|
||||
q87 -162 364 -193h297z" />
|
||||
<glyph glyph-name="u1D30D" unicode="𝌍" horiz-adv-x="1228"
|
||||
d="M154 294q0 190 133 323q138 138 327 138q195 0 323 -138q138 -128 138 -323q0 -189 -138 -327q-133 -133 -323 -133q-184 0 -327 133q-133 143 -133 327zM251 494h30q26 -30 32 -30q25 -93 55 -108l26 -15q16 0 16 25q0 20 -10 31q0 20 4 25q11 21 21 26q10 20 15 20
|
||||
l11 -15q15 -30 30 -36v-10q6 -15 6 -20v-10q0 -6 -6 -6q0 -5 -8 -5h-12q5 0 5 -10q6 -11 10 -15l11 -26l15 -26q5 -10 15 -20q16 -31 36 -36q21 -15 56 -15v-21l11 -5q5 0 5 -5q20 0 30 5l6 -5q-6 -20 -47 -41l-40 -15v-11q0 -5 -3 -15t-3 -25q-10 -57 -15 -62q26 -6 41 -5
|
||||
q10 0 41 10l41 10q15 -10 25 -25v-15l16 -6v-20l-5 -6l5 -10h15q31 0 36 36q41 16 61 46q21 26 21 72l-5 5q-21 36 -21 46q-15 31 -15 41q10 -15 30 -15h6l26 5l-16 25v6l20 -21l-10 -26l26 16l15 -5q16 5 26 30h-10v26l-16 -10l-5 5v5q-15 0 -20 5q-6 0 -6 11v4
|
||||
q-36 21 -46 21v-21v-10h-5q-25 31 -56 31h-20q-11 10 -21 10h-10v16l-10 5q0 5 -3 15t-3 20q-5 21 -15 32l5 10h-20q-10 36 -21 35l5 26v20l-15 6q-26 0 -26 -26l16 -31q-10 -20 -10 -25q0 -16 5 -21l-5 -10q-10 -10 -11 -20v-6l-46 -35q-5 0 -5 10v10v10q0 16 -15 36l15 -5
|
||||
q16 0 26 36v20q0 21 -6 26q11 0 11 25q56 11 56 72q0 16 -15 46l-10 10h5q15 -26 30 -25q6 0 16 10q5 10 5 21q0 15 -11 25l-4 5v6q20 10 20 15q5 5 5 15v31q-51 10 -72 10q-225 0 -363 -215zM650 305v20q0 -5 11 -15zM676 238v16q0 10 5 15v-31h20v5v-5h6q10 0 15 5l10 -5
|
||||
q16 -5 16 -15q0 -5 -6 -11h-10q-20 0 -30.5 -10t-15.5 -10l-5 5q20 0 20 21v15h-15l-5 5h-5zM758 207q5 -5 10 -5v-10q-5 -10 -5 -15v10q-5 5 -5 20zM891 141q0 -16 15 -31q10 -10 26 -10l10 -5q10 0 10 -5q-15 51 -25 56q-16 16 -31 25q0 -10 -5 -20v-10z" />
|
||||
<glyph glyph-name="u1F304" unicode="🌄"
|
||||
d="M0 -7v672q0 24 17.5 41.5t41.5 17.5h829q25 0 42 -17.5t17 -41.5v-672q0 -24 -17 -41.5t-42 -17.5h-829q-24 0 -41.5 17.5t-17.5 41.5zM79 13h789v632h-789v-632zM158 92v54l142 185l97 -80l182 276l210 -218v-217h-631zM158 486q0 33 23 56.5t57 23.5q33 0 56 -23.5
|
||||
t23 -56.5t-23 -56t-56 -23q-34 0 -57 23t-23 56z" />
|
||||
<glyph glyph-name="u1F310" unicode="🌐" horiz-adv-x="1228"
|
||||
d="M307 -115q0 51 51 51h205v61q-127 16 -235 113q-11 10 -11 36q0 21 11 31q15 15 30 15q21 0 36 -15q72 -72 169 -87h102q98 15 170 87q92 92 92 220t-92 220l-31 36l138 133q10 15 30.5 15t31.5 -15q15 -10 15 -30.5t-15 -30.5l-72 -77q87 -113 87 -251q0 -159 -118 -287
|
||||
q-107 -97 -236 -113v-61h206q51 0 51 -51t-51 -51h-513q-51 0 -51 51zM364 396.5q0 102.5 74 177t176.5 74.5t176.5 -74.5t74 -177t-74 -176.5t-176.5 -74t-176.5 74t-74 176.5z" />
|
||||
<glyph glyph-name="u1F381" unicode="🎁"
|
||||
d="M0 230v198q0 8 6 14t14 6h272q-29 0 -54 10.5t-44 29.5t-30 44t-11 53.5t11 54t30 44t44 29.5t54 11q30 0 57.5 -12.5t45.5 -35.5l79 -102l79 102q18 23 45 35.5t58 12.5q28 0 53.5 -11t44.5 -29.5t30 -44t11 -54t-11 -53.5t-30 -44t-44.5 -29.5t-53.5 -10.5h271
|
||||
q9 0 14.5 -6t5.5 -14v-198q0 -8 -5.5 -13.5t-14.5 -5.5h-59v-218q0 -24 -17 -41.5t-42 -17.5h-671q-24 0 -41.5 17.5t-17.5 41.5v218h-59q-8 0 -14 5.5t-6 13.5zM232 586q0 -24 17.5 -41.5t42.5 -17.5h119l-77 99q-6 5 -16.5 12t-25.5 7q-25 0 -42.5 -17.5t-17.5 -41.5z
|
||||
M375 45q0 -16 11.5 -27.5t28.5 -11.5h118q16 0 28 11.5t12 27.5v403h-198v-403zM536 527h120q24 0 42 17.5t18 41.5t-18 41.5t-42 17.5q-16 0 -26.5 -7t-15.5 -12z" />
|
||||
<glyph glyph-name="u1F3A4" unicode="🎤" horiz-adv-x="670"
|
||||
d="M635 438q8 0 14 -6t6 -14v-138q0 -93 -69 -165t-201 -83v-132h130q8 0 14 -6t6 -14v-60q0 -8 -6 -14t-14 -6h-360q-8 0 -14 6t-6 14v60q0 8 6 14t14 6h130v132q-132 11 -201 83t-69 165v138q0 8 6 14t14 6h30q8 0 14 -6t6 -14v-138q0 -28 12 -57t38.5 -58t78 -47
|
||||
t121.5 -18t121.5 18t78 47t38.5 58t12 57v138q0 8 6 14t14 6h30zM335 200q-79 0 -114.5 25t-35.5 55v158h300v-158q0 -30 -35.5 -55t-114.5 -25zM485 720v-212h-300v212q0 30 35.5 55t114.5 25t114.5 -25t35.5 -55z" />
|
||||
<glyph glyph-name="u1F3A7" unicode="🎧"
|
||||
d="M0 309q0 57 19.5 110t52.5 99t79 83.5t98.5 65t110 42.5t114.5 15t114 -15t110 -42.5t98.5 -65t78.5 -83.5t52.5 -99t19.5 -110q0 -92 -38 -174l-13 -29l-85 -13q-14 -51 -55.5 -85t-97.5 -34v-20q0 -8 -6 -14t-14 -6h-40q-9 0 -14 6t-5 14v355q0 8 5 14t14 6h40
|
||||
q8 0 14 -6t6 -14v-19q44 0 79.5 -22t56.5 -57l20 2q15 45 15 96q0 61 -32.5 115t-84 94t-114.5 64t-124 24t-124.5 -24t-114.5 -64t-83.5 -94t-32.5 -115q0 -48 15 -96l20 -2q21 35 56 57t79 22v19q0 8 6 14t14 6h40q9 0 14 -6t5 -14v-355q0 -8 -5 -14t-14 -6h-40
|
||||
q-8 0 -14 6t-6 14v20q-55 0 -96.5 34t-55.5 85l-85 13l-14 29q-38 82 -38 174z" />
|
||||
<glyph glyph-name="u1F3C6" unicode="🏆" horiz-adv-x="930"
|
||||
d="M524 82v-65q71 -8 117 -32t46 -55q0 -37 -65 -63.5t-157 -26.5t-157 26.5t-65 63.5q0 31 46 55t117 32v65q0 50 -33 85t-112 87q-45 30 -69 47.5t-63.5 54.5t-60 72t-37 88.5t-16.5 115.5q0 14 10 24.5t25 10.5h172q21 39 81.5 66t161.5 27t161.5 -27t81.5 -66h172
|
||||
q15 0 25 -10.5t10 -24.5q0 -62 -16.5 -115.5t-37 -88.5t-60 -72t-63.5 -54.5t-69 -47.5q-79 -52 -112 -87t-33 -85zM663 335q80 54 126 112t54 150h-126q-5 -157 -54 -262zM465 699q-39 0 -72 -6t-54 -15t-36 -20t-21.5 -20t-6.5 -16t6.5 -16t21.5 -20t36 -20t54 -15t72 -6
|
||||
t72 6t54 15t36 20t21.5 20t6.5 16t-6.5 16t-21.5 20t-36 20t-54 15t-72 6zM87 597q8 -92 54 -150t126 -112q-49 105 -54 262h-126z" />
|
||||
<glyph glyph-name="u1F3C9" unicode="🏉" horiz-adv-x="868"
|
||||
d="M0 470v96q0 17 11.5 28t28.5 11h166q-1 6 -1 13v12v2q0 27 2 45t8 28t16.5 14.5t28.5 4.5h348q17 0 28 -4.5t17 -14.5t8 -28t2 -45v-14q0 -6 -1 -13h166q17 0 28.5 -11t11.5 -28v-96q0 -33 -23 -67.5t-62.5 -65.5t-92.5 -55t-114 -33q-27 -5 -47.5 -21.5t-20.5 -36.5
|
||||
q0 -18 9 -26.5t20 -15.5t20.5 -16t11.5 -27q3 -12 -1 -24q-2 -8 12.5 -12.5t35 -9.5t42 -11.5t33.5 -17.5q6 -5 9.5 -20.5t4.5 -33.5q1 -16 -3 -29.5t-15 -13.5h-506q-10 0 -14.5 13.5t-3.5 29.5q1 18 5 33.5t10 20.5q11 11 32.5 17.5t42 11.5t35 9.5t12.5 12t-2 12.5v12
|
||||
q1 18 11 27t21.5 16t20.5 15.5t9 26.5q0 20 -20.5 36.5t-47.5 21.5q-60 10 -113.5 34t-93 54.5t-62.5 65t-23 67.5zM79 470q0 -11 12 -28t34.5 -36t55 -37t72.5 -31q-13 41 -23 90t-17 98h-134v-56zM616 338q40 13 72 31t54.5 37t34.5 36t12 28v56h-134q-7 -49 -17 -98
|
||||
t-22 -90z" />
|
||||
<glyph glyph-name="u1F44D" unicode="👍" horiz-adv-x="824"
|
||||
d="M0.5 168q-1.5 59 2 118t13.5 110q47 3 98.5 3t94.5 -10q7 -40 11 -96t5.5 -116t0.5 -116.5t-5 -98.5q-19 -3 -43.5 -3.5t-51.5 1t-53 2t-47 0.5q-10 40 -17 93.5t-8.5 112.5zM96 41q0 -17 11.5 -28.5t28.5 -11.5t28 11.5t11 28.5q0 15 -11 26.5t-28 11.5t-28.5 -11.5
|
||||
t-11.5 -26.5zM253 368q23 10 37 20t25.5 22.5t24 27.5t32.5 36q16 17 29.5 28t24.5 21t20 21t18 27q17 31 22.5 68.5t13.5 71.5q0 7 7 12q20 3 37 -4t29.5 -18.5t20.5 -27.5t11 -29q7 -35 -1 -63t-20 -53l-24 -49q-11 -24 -12 -50q23 -10 55 -10.5t66.5 1.5t67 2t55 -9
|
||||
t30 -30.5t-5.5 -63.5q0 -2 -2 -5l-6 -8q-3 -5 -5 -10l-2 -3q12 -11 17 -24t5 -21q1 -41 -34 -72q11 -15 12 -32.5t-4 -33.5t-15 -28t-23 -18q6 -36 -6.5 -61.5t-37 -40.5t-59.5 -21.5t-73 -6.5t-76.5 5.5t-70.5 14.5q-21 7 -41 15l-41 15q-20 8 -43 12.5t-48 -0.5
|
||||
q2 41 2.5 89.5t-1 98.5t-4.5 97.5t-7 86.5z" />
|
||||
<glyph glyph-name="u1F44E" unicode="👎" horiz-adv-x="824"
|
||||
d="M1 410q-1 22 8 39.5t26 32.5q-10 15 -11 32.5t4 33.5t15 28t22 18q-6 36 6.5 61.5t37.5 40.5t59.5 21.5t72.5 6.5t76.5 -5.5t70.5 -14.5q21 -7 41 -15t41 -15.5t43.5 -11.5t47.5 1q-2 -41 -2.5 -90t1 -99t4 -97.5t8.5 -86.5q-24 -11 -38 -21t-25 -22t-23.5 -27t-32.5 -35
|
||||
q-16 -18 -29 -29l-24 -21q-12 -10 -21.5 -20.5t-18.5 -27.5q-17 -31 -21.5 -68.5t-14.5 -71.5q0 -9 -7 -12q-21 -3 -37.5 4t-29 18.5t-20.5 26.5t-11 30q-6 35 1.5 63t20 53t23.5 49.5t12 50.5q-22 9 -54.5 9t-67 -1.5t-66.5 -1.5t-54.5 9t-30.5 30.5t5 62.5q1 1 4 6l6 9
|
||||
q2 4 3 9l2 3q-11 11 -16.5 24t-5.5 21zM598 597.5q1 56.5 5 98.5q19 2 44.5 2.5t52 0t52.5 -1.5t47 -1q10 -40 16.5 -93.5t8 -112t-2 -118t-13.5 -110.5q-47 -4 -98 -4t-95 11q-7 40 -11 96t-5.5 116t-0.5 116.5zM649 618q0 -16 12 -28t28 -12t27.5 12t11.5 28t-11.5 27.5
|
||||
t-27.5 11.5t-28 -11.5t-12 -27.5z" />
|
||||
<glyph glyph-name="u1F464" unicode="👤" horiz-adv-x="970"
|
||||
d="M751 78q94 -34 149 -67t55 -56v-105h-940v105q0 23 55 56t149 67q93 34 128 69t35 95q0 20 -10 31.5t-24 31.5t-21 58q-2 12 -9 17.5t-14.5 8t-14 17t-8.5 43.5q0 15 4.5 25.5t8.5 13.5l5 3q-9 50 -13 88q-2 21 6 47.5t27.5 55.5t62.5 48.5t103 19.5t103 -19.5
|
||||
t62.5 -48.5t27.5 -55.5t6 -47.5l-13 -88q18 -8 18 -42q-2 -29 -8.5 -43.5t-14 -17t-14.5 -8t-9 -17.5q-7 -38 -21 -58t-24 -31.5t-10 -31.5q0 -60 35 -95t128 -69z" />
|
||||
<glyph glyph-name="u1F465" unicode="👥" horiz-adv-x="1030"
|
||||
d="M1015 -125h-225v144q0 52 -30 78.5t-154 85.5q41 29 41 81q0 14 -13.5 31.5t-18.5 49.5q-1 8 -5.5 11.5t-8.5 5t-8.5 11.5t-5.5 29q0 10 2.5 17t5.5 9l3 2q-6 33 -8 59q-1 14 3.5 31.5t16.5 37t38 32.5t62 13t62 -13t37.5 -32.5t16 -37t3.5 -31.5l-7 -59q10 -6 10 -28
|
||||
q-1 -19 -5.5 -29t-8.5 -11.5t-8.5 -5t-5.5 -11.5q-5 -32 -18.5 -49.5t-13.5 -31.5q0 -40 21 -63.5t77 -46.5q61 -25 90 -41t41 -36q5 -8 8.5 -58.5t4.5 -96.5zM526 127q89 -36 136.5 -64t47.5 -56v-132h-695v176q0 20 15.5 36t28.5 22.5t35 15.5q3 1 5 2q75 30 103 61t28 85
|
||||
q0 19 -18.5 42t-25.5 66q-2 10 -7.5 15t-11.5 7t-11 15t-7 39q0 13 3.5 22.5t7.5 12.5l4 2q-8 45 -11 79q-2 19 4.5 42t22 49t50.5 43.5t83 17.5t82.5 -17.5t50 -43.5t22 -49t4.5 -42l-10 -79q14 -7 14 -37q-2 -26 -7.5 -39t-11 -15t-11 -7t-7.5 -15q-7 -43 -25 -66t-18 -42
|
||||
q0 -54 28 -85t102 -61z" />
|
||||
<glyph glyph-name="u1F4A1" unicode="💡" horiz-adv-x="670"
|
||||
d="M473 10h-265q0 59 -21.5 114.5t-50.5 97.5l-59 86q-29 43 -47 97t-14 112q3 43 14.5 81.5t36 76t59 64.5t88.5 44t121 17t121 -17t88.5 -44t59 -64.5t36 -76t14.5 -81.5q4 -59 -12.5 -113t-43.5 -98l-56 -86q-29 -44 -49 -98t-20 -112zM203 -159v99h264v-99
|
||||
q-60 -42 -132 -41q-72 -1 -132 41z" />
|
||||
<glyph glyph-name="u1F4B0" unicode="💰" horiz-adv-x="803"
|
||||
d="M312 349q17 20 66 23v-84q-43 3 -59.5 10t-16.5 28q0 13 10 23zM420 108v91q48 -4 63 -10.5t15 -24.5q0 -45 -78 -56zM490 351q22 -12 29 -19.5t7 -17.5l8 -4l45 90l-7 5q-10 -7 -13 -7q-8 0 -13 3q-90 35 -126 35v18q0 14 20 18v9h-79v-9q17 -4 17 -18v-15
|
||||
q-73 -3 -113 -35.5t-40 -87.5t33.5 -78.5t119.5 -31.5v-96q-56 5 -86.5 23.5t-30.5 40.5l-7 4l-42 -94l7 -4q9 5 12 5q7 0 9 -3q72 -36 138 -40v-18q0 -15 -17 -20v-9h79v9q-20 5 -20 20v18q72 4 115.5 38.5t43.5 88.5q0 101 -149 115h-10v87q36 -2 70 -20zM531 532
|
||||
q89 -37 143.5 -118.5t54.5 -181.5q0 -136 -95.5 -231.5t-231.5 -95.5t-231.5 95.5t-95.5 231.5q0 100 54.5 181.5t142.5 118.5l-81 179q0 25 28 25h366q27 0 27 -25z" />
|
||||
<glyph glyph-name="u1F4B5" unicode="💵" horiz-adv-x="1293"
|
||||
d="M79 291q76 0 149 24t134.5 62t121.5 85l120 92q61 46 122.5 84t134.5 62.5t149 24.5l105 -227l90 -192q-103 0 -199.5 -42t-176.5 -102l-160 -122q-80 -62 -182.5 -110.5t-212.5 -57.5l-98 210zM199 225l63 -137l64 -136q132 26 283 137q-70 27 -107 103q-32 72 -20 168
|
||||
q-154 -112 -283 -135zM1085 372l-68 145l-59 128q-136 -25 -284 -137q67 -24 106 -103q34 -75 20 -167q151 111 285 134zM428 68q-4 6 -10 18t-8 18q-5 -3 -10 -3q1 -11 6 -40q15 3 22 7zM455 90q4 6 11 21q-24 7 -38 13q0 -2 -1.5 -3.5t-3 -3.5t-1.5 -3zM371 68q1 3 6 15.5
|
||||
t8 18.5q-5 3 -9 6q-4 -6 -13 -15t-13 -14q12 -10 21 -11zM468 148q-3 14 -6 22h-1q-15 -10 -35 -20v-1q3 -3 3 -10q21 3 39 9zM329 106q27 10 37 15q-2 8 -2 9q-14 -1 -43 -1q1 -11 8 -23zM416 161q4 5 12 17l12 17q-10 9 -19 10q-10 -25 -13 -39q1 0 4 -2t4 -3zM365 145
|
||||
q3 3 5 10l-16 13l-15 12q-8 -8 -11 -20q3 -1 11.5 -4.5t14.5 -5.5t11 -5zM392 168q0 7 -2 20t-2 20q-8 -2 -23 -7q3 -3 5 -9t4 -10q-2 4 -4 10t-5 9q3 -6 8.5 -18.5t8.5 -18.5q2 1 10 4zM872 404q-8 22 -43 94q-13 -7 -19 -12q8 -16 22 -47l20 -47q12 8 20 12zM960 446
|
||||
q-5 13 -20.5 44t-23.5 49q-4 -1 -10.5 -4.5t-9.5 -4.5l44 -91q11 3 20 7zM916 426q-6 15 -14 30l-16 35l-15 29q-3 -2 -9 -5l-10 -5q26 -51 44 -92q4 1 11 4t9 4z" />
|
||||
<glyph glyph-name="u1F4C2" unicode="📂"
|
||||
d="M0 129v536q0 24 17.5 41.5t41.5 17.5h356q24 0 41.5 -17.5t17.5 -41.5t17 -41.5t42 -17.5h227q24 0 41.5 -18t17.5 -42v-114h-632q-19 0 -36.5 -6.5t-33.5 -17.5t-27.5 -26t-17.5 -33zM22 -66l126 391q4 11 16.5 19.5t23.5 8.5h759l-133 -389q-3 -12 -16 -21t-24 -9h-752
|
||||
z" />
|
||||
<glyph glyph-name="u1F4C4" unicode="📄" horiz-adv-x="631"
|
||||
d="M0 -7v672q0 24 17.5 41.5t41.5 17.5h230v-283q0 -25 17.5 -42t41.5 -17h284v-389q0 -24 -17.5 -41.5t-41.5 -17.5h-514q-24 0 -41.5 17.5t-17.5 41.5zM348 441v280h4l276 -277v-3h-280z" />
|
||||
<glyph glyph-name="u1F4C5" unicode="📅" horiz-adv-x="789"
|
||||
d="M0 -7v578q0 24 17.5 41.5t41.5 17.5h38v-73q0 -29 20.5 -49.5t49.5 -20.5h16q30 0 51 20.5t21 49.5v73h61v-73q0 -29 20.5 -49.5t49.5 -20.5h17q30 0 50.5 20.5t20.5 49.5v73h61v-73q0 -29 21 -49.5t50 -20.5h16q30 0 50.5 20.5t20.5 49.5v73h38q24 0 41 -17.5t17 -41.5
|
||||
v-578q0 -24 -17 -41.5t-41 -17.5h-672q-24 0 -41.5 17.5t-17.5 41.5zM79 13h632v419h-632v-419zM136 557v135q0 13 9.5 22.5t21.5 9.5h16q14 0 23 -9.5t9 -22.5v-135q0 -12 -9 -21t-23 -9h-16q-12 0 -21.5 9t-9.5 21zM186 101q0 37 21 61t46 42l46 33q21 16 21 38
|
||||
q0 21 -12 30.5t-30 9.5q-12 0 -22 -4.5t-17 -12.5q-4 -4 -7.5 -8t-6.5 -9l-36 24q8 14 22 28q11 12 28.5 20.5t42.5 8.5q37 0 64.5 -22t27.5 -62q0 -22 -9.5 -38t-24.5 -29.5t-32 -24.5l-31 -20q-15 -11 -25 -22.5t-10 -26.5h97v36h44v-77h-195q-1 7 -1.5 13t-0.5 12z
|
||||
M356 557v135q0 13 9 22.5t21 9.5h17q13 0 22.5 -9.5t9.5 -22.5v-135q0 -12 -9.5 -21t-22.5 -9h-17q-12 0 -21 9t-9 21zM418 282v74h197v-35l-123 -245h-54l113 223q3 9 6 12l3 4v1q-3 0 -5 -1h-14h-79v-33h-44zM575 557v135q0 13 9 22.5t22 9.5h16q13 0 22.5 -9.5t9.5 -22.5
|
||||
v-135q0 -12 -9.5 -21t-22.5 -9h-16q-13 0 -22 9t-9 21z" />
|
||||
<glyph glyph-name="u1F4C8" unicode="📈" horiz-adv-x="1228"
|
||||
d="M205 -64v666h102v-374l251 358l220 -276l113 77l61 -82l-194 -134l-195 246l-256 -363v-16h666v-102h-768z" />
|
||||
<glyph glyph-name="u1F4CA" unicode="📊" horiz-adv-x="1228"
|
||||
d="M256 -64v307h205v-307h-205zM512 -59v712h205v-712h-205zM768 -59v507h205v-507h-205z" />
|
||||
<glyph glyph-name="u1F4CC" unicode="📌"
|
||||
d="M1 419q4 40 22 85t49.5 87t69 72t75.5 45.5t72 15t59 -19.5q27 -19 37 -52.5t6 -74.5l148 -110q55 30 107 35t89 -23q26 -20 39 -53t12 -74t-15 -88t-42 -93l212 -203q5 -5 6 -12.5t-4 -12.5q-5 -9 -16 -9q-3 0 -9 2l-255 146q-37 -39 -77.5 -66.5t-79.5 -40t-74.5 -10
|
||||
t-63.5 23.5q-36 27 -46.5 78t3.5 112l-148 112q-38 -16 -73 -16t-61 19q-25 19 -35.5 52t-6.5 73zM84 374q1 -8 9 -13q8 -8 23 -8q16 0 33 8.5t35 23t35.5 33t33.5 39.5q5 7 4 14.5t-9 13.5q-6 5 -14 4t-13 -8q-39 -52 -69 -71.5t-36 -17.5q-7 5 -14.5 4t-13.5 -8
|
||||
q-5 -6 -4 -14zM231 322l178 -134q7 -4 12 -4q11 0 16 8q5 6 4 14t-7 13l-172 128q-16 -15 -31 -25zM414 83q-11 -17 4 -29q14 -10 35 -10q20 0 43 10.5t46.5 28.5t47 41t43.5 49q5 8 4 15.5t-9 13.5q-6 5 -14 3.5t-13 -8.5q-25 -33 -50.5 -57t-47.5 -37.5t-38.5 -17.5
|
||||
t-23.5 2q-6 5 -14 3.5t-13 -7.5z" />
|
||||
<glyph glyph-name="u1F4CE" unicode="📎" horiz-adv-x="965"
|
||||
d="M259 -192q-58 0 -110 26.5t-84.5 72t-45.5 100t6.5 116t72.5 114.5l222 222l273 274q37 37 83.5 51t92.5 2q45 -12 79 -46t46 -79q12 -46 -2 -92.5t-51 -83.5l-474 -473q-24 -24 -52 -37t-60 -10t-56 27q-19 19 -25.5 46t4.5 60t40 62l333 332q11 10 25 10t24 -10t10 -24
|
||||
t-10 -25l-332 -333q-21 -21 -25.5 -40.5t5.5 -27.5q9 -9 23 -7q25 3 47 26l473 474q50 52 35 108q-8 27 -28 47t-47 28q-57 16 -108 -35l-274 -273l-222 -222q-44 -44 -56.5 -94t1 -91t45.5 -73t73 -45.5t91 -1t94 56.5l495 496q10 10 25 10t25 -10t10 -25t-10 -25
|
||||
l-496 -495q-83 -83 -185 -83z" />
|
||||
<glyph glyph-name="u1F4D6" unicode="📖" horiz-adv-x="930"
|
||||
d="M355 186v-67l-200 80v67zM355 394v-68l-200 80v68zM846 746q26 10 47.5 -6t21.5 -40v-640q0 -33 -31 -46l-400 -160q-2 0 -5.5 -1.5t-7 -2t-6.5 -0.5t-6.5 0.5t-7 2t-5.5 1.5l-400 160q-31 13 -31 46v640q0 24 21.5 40t47.5 6l381 -152zM415 -48v561l-320 128v-561z
|
||||
M835 80v561l-320 -128v-561zM775 266v-67l-200 -80v67zM775 474v-68l-200 -80v68z" />
|
||||
<glyph glyph-name="u1F50D" unicode="🔍" horiz-adv-x="820"
|
||||
d="M787 27q34 -34 7 -61l-47 -47q-14 -14 -33.5 -14t-33.5 14l-190 190q-72 -42 -156 -42q-128 0 -223.5 95.5t-95.5 223.5t90.5 218.5t218.5 90.5t223.5 -95.5t95.5 -223.5q0 -84 -45 -160zM110 386q0 -88 68 -156t156 -68t151 63t63 151t-68 156t-156 68t-151 -63
|
||||
t-63 -151z" />
|
||||
<glyph glyph-name="u1F512" unicode="🔒" horiz-adv-x="730"
|
||||
d="M655 425q21 0 40.5 -19.5t19.5 -40.5v-390q0 -21 -14 -40t-34 -25l-59 -20q-49 -15 -98 -15h-290q-49 0 -98 15l-59 20q-20 6 -34 25t-14 40v390q0 22 15 41t35 19h100v70q0 110 51 170t149 60t149 -60t51 -170v-70h90zM265 515v-90h200v90q0 53 -27 81.5t-73 28.5
|
||||
t-73 -28.5t-27 -81.5z" />
|
||||
<glyph glyph-name="u1F513" unicode="🔓" horiz-adv-x="730"
|
||||
d="M655 400q21 0 40.5 -19.5t19.5 -40.5v-390q0 -21 -14 -40t-34 -25l-59 -20q-49 -15 -98 -15h-290q-49 0 -98 15l-59 20q-20 6 -34 25t-14 40v390q0 22 15 41t35 19h400v140q0 53 -27 81.5t-73 28.5t-73 -28.5t-27 -81.5v-40h-100v20q0 110 51 170t149 60t149 -60t51 -170
|
||||
v-120h90z" />
|
||||
<glyph glyph-name="u1F516" unicode="🔖" horiz-adv-x="631"
|
||||
d="M0 -9v675q0 18 9.5 32t26.5 21q5 3 10.5 4t11.5 1h516q5 0 11 -1t11 -4q17 -7 26.5 -21t9.5 -32v-675q0 -18 -9.5 -32t-26.5 -21q-16 -7 -33.5 -3.5t-28.5 16.5l-218 218l-218 -218q-12 -13 -29 -16.5t-33 3.5q-17 8 -26.5 21.5t-9.5 31.5z" />
|
||||
<glyph glyph-name="u1F517" unicode="🔗"
|
||||
d="M0 92v474q0 32 12.5 61t33.5 50t50 34t62 13h299q-1 -8 -2 -14.5t-1 -15.5v-45q0 -12 3 -24h-299q-24 0 -41.5 -17.5t-17.5 -41.5v-474q0 -24 17.5 -41.5t41.5 -17.5h553q24 0 41 17.5t17 41.5v175q22 -16 47 -25.5t52 -10.5v-139q0 -33 -12.5 -61t-34 -50t-50 -34.5
|
||||
t-60.5 -12.5h-553q-33 0 -62 12.5t-50 34.5t-33.5 50t-12.5 61zM356 202.5q0 12.5 8 21.5l396 397h-138q-13 0 -21 8t-8 21v44q-1 12 7.5 21t21.5 9h296q11 0 20 -9t9 -21v-44v-252q0 -12 -9 -21t-20 -8h-44q-13 0 -21.5 8.5t-8.5 20.5v138l-397 -395q-8 -9 -20.5 -9
|
||||
t-21.5 9l-41 41q-8 8 -8 20.5z" />
|
||||
<glyph glyph-name="u1F525" unicode="🔥" horiz-adv-x="710"
|
||||
d="M0 159q0 62 32 126.5t87 117.5q-12 -79 0.5 -127t32.5 -75q23 -32 55 -47q-25 111 -14 215q4 44 16 92t35.5 95t61.5 90t94 78q-24 -52 -23 -95t11 -75q12 -37 36 -67l32 -39q16 -20 27 -45.5t17 -59.5t6 -82q-9 21 -28 33.5t-43 12.5q-34 0 -56.5 -23t-22.5 -56
|
||||
q0 -17 5.5 -31.5t18 -26.5t32 -18t46.5 -6q46 4 81 32q14 13 27 31.5t21.5 45.5t10.5 63t-5 85h-1q54 -53 86.5 -117.5t32.5 -126.5q0 -56 -28 -98.5t-76.5 -70t-113 -42t-138 -14.5t-138 14.5t-113 42t-76.5 70t-28 98.5z" />
|
||||
<glyph glyph-name="u1F554" unicode="🕔" horiz-adv-x="950"
|
||||
d="M475 760q125 0 231 -61.5t167.5 -167.5t61.5 -231t-61.5 -231t-167.5 -167.5t-231 -61.5t-231 61.5t-167.5 167.5t-61.5 231t61.5 231t167.5 167.5t231 61.5zM475 -60q149 0 254.5 105.5t105.5 254.5t-105.5 254.5t-254.5 105.5t-254.5 -105.5t-105.5 -254.5
|
||||
t105.5 -254.5t254.5 -105.5zM510 560v-246l150 -149l-50 -50l-170 170v275h70z" />
|
||||
<glyph glyph-name="u1F6AB" unicode="🚫" horiz-adv-x="950"
|
||||
d="M475 760q125 0 231 -61.5t167.5 -167.5t61.5 -231t-61.5 -231t-167.5 -167.5t-231 -61.5t-231 61.5t-167.5 167.5t-61.5 231t61.5 231t167.5 167.5t231 61.5zM723 547h-1h1zM125 300q0 -125 79 -222l492 493q-97 79 -221 79q-145 0 -247.5 -102.5t-102.5 -247.5zM227 53
|
||||
l1 -1zM475 -50q145 0 247.5 102.5t102.5 247.5q0 124 -79 221l-492 -492q97 -79 221 -79z" />
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
Before Width: | Height: | Size: 63 KiB |
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user