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 |
@@ -0,0 +1,2 @@
|
|||||||
|
.git
|
||||||
|
web/vue/node_modules
|
||||||
+11
-11
@@ -23,18 +23,18 @@ _testmain.go
|
|||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
log/*
|
log
|
||||||
data/*
|
data
|
||||||
conf/install.lock
|
conf
|
||||||
conf/app.ini
|
|
||||||
conf/ansible_hosts.ini
|
|
||||||
profile/*
|
profile/*
|
||||||
public/resource/javascript/vue.js
|
/gocron
|
||||||
gocron
|
/gocron-node
|
||||||
gocron.exe
|
/bin
|
||||||
gocron-node
|
/web/public/static
|
||||||
gocron-node.exe
|
/web/public/index.html
|
||||||
|
/gocron-package
|
||||||
|
/gocron-node-package
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
package.json
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.7.x
|
|
||||||
script: go test `go list ./... | grep -v vendor`
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
on_success: never
|
|
||||||
on_failure: always
|
|
||||||
+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,29 +1,32 @@
|
|||||||
[](https://travis-ci.org/ouqiang/gocron)
|
|
||||||
# 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)
|
原有的延时任务拆分为独立项目[延迟队列](https://github.com/ouqiang/delay-queue)
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
* Web界面管理定时任务, 支持动态添加、删除任务
|
* Web界面管理定时任务
|
||||||
* crontab时间表达式,精确到秒
|
* crontab时间表达式, 精确到秒
|
||||||
* 任务执行失败重试设置
|
* 任务执行失败可重试
|
||||||
* 任务超时设置
|
* 任务执行超时, 强制结束
|
||||||
* 任务依赖配置
|
* 任务依赖配置, A任务完成后再执行B任务
|
||||||
|
* 账户权限控制
|
||||||
* 任务类型
|
* 任务类型
|
||||||
* shell任务
|
* shell任务
|
||||||
> 在任务节点上执行shell命令, 支持任务同时在多个节点上运行
|
> 在任务节点上执行shell命令, 支持任务同时在多个节点上运行
|
||||||
* HTTP任务
|
* HTTP任务
|
||||||
> 访问指定的URL地址, 由调度器直接执行, 不依赖任务节点
|
> 访问指定的URL地址, 由调度器直接执行, 不依赖任务节点
|
||||||
* 查看任务执行日志
|
* 查看任务执行结果日志
|
||||||
* 任务执行结果通知, 支持邮件、Slack
|
* 任务执行结果通知, 支持邮件、Slack、Webhook
|
||||||
|
|
||||||
### 截图
|
### 截图
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
### 支持平台
|
### 支持平台
|
||||||
> Windows、Linux、Mac OS
|
> Windows、Linux、Mac OS
|
||||||
@@ -33,8 +36,9 @@
|
|||||||
|
|
||||||
|
|
||||||
## 下载
|
## 下载
|
||||||
[v1.1](https://github.com/ouqiang/gocron/releases/tag/v1.1)
|
[releases](https://github.com/ouqiang/gocron/releases)
|
||||||
|
|
||||||
|
[版本升级](https://github.com/ouqiang/gocron/wiki/版本升级)
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -51,30 +55,81 @@
|
|||||||
4. 浏览器访问 http://localhost:5920
|
4. 浏览器访问 http://localhost:5920
|
||||||
|
|
||||||
### 源码安装
|
### 源码安装
|
||||||
1. `go`语言版本1.7+
|
|
||||||
2. `go get -d github.com/ouqiang/gocron`
|
- 安装Go 1.11+
|
||||||
3. 编译
|
- `go get -d github.com/ouqiang/gocron`
|
||||||
* 调度器 `go build -tags gocron -o gocron`
|
- `export GO111MODULE=on`
|
||||||
* 任务节点 `go build -tags node -o gocron-node`
|
- 编译 `make`
|
||||||
4. 启动、访问方式同上
|
- 启动
|
||||||
|
* 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
|
* gocron web
|
||||||
* --host 默认0.0.0.0
|
* --host 默认0.0.0.0
|
||||||
* -p 端口, 指定端口, 默认5920
|
* -p 端口, 指定端口, 默认5920
|
||||||
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
|
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
|
||||||
* -h 查看帮助
|
* -h 查看帮助
|
||||||
* gocron-node
|
* gocron-node
|
||||||
* -allow-root *nix平台允许以root用户运行
|
* -allow-root *nix平台允许以root用户运行
|
||||||
* -s ip:port 监听地址
|
* -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)
|
* 定时任务调度 [Cron](https://github.com/robfig/cron)
|
||||||
* ORM [Xorm](https://github.com/go-xorm/xorm)
|
* ORM [Xorm](https://github.com/go-xorm/xorm)
|
||||||
* UI框架 [Semantic UI](https://semantic-ui.com/)
|
* UI框架 [Element UI](https://github.com/ElemeFE/element)
|
||||||
* 依赖管理(所有依赖包放入vendor目录) [Govendor](https://github.com/kardianos/govendor)
|
* 依赖管理 [Govendor](https://github.com/kardianos/govendor)
|
||||||
* RPC框架 [gRPC](https://github.com/grpc/grpc)
|
* RPC框架 [gRPC](https://github.com/grpc/grpc)
|
||||||
|
|
||||||
## 反馈
|
## 反馈
|
||||||
@@ -82,6 +137,36 @@
|
|||||||
|
|
||||||
## ChangeLog
|
## ChangeLog
|
||||||
|
|
||||||
|
v1.5
|
||||||
|
--------
|
||||||
|
* 前端使用Vue+ElementUI重构
|
||||||
|
* 任务通知
|
||||||
|
* 新增WebHook通知
|
||||||
|
* 自定义通知模板
|
||||||
|
* 匹配任务执行结果关键字发送通知
|
||||||
|
* 任务列表页显示任务下次执行时间
|
||||||
|
|
||||||
|
v1.4
|
||||||
|
--------
|
||||||
|
* HTTP任务支持POST请求
|
||||||
|
* 后台手动停止运行中的shell任务
|
||||||
|
* 任务执行失败重试间隔时间支持用户自定义
|
||||||
|
* 修复API接口调用报403错误
|
||||||
|
|
||||||
|
v1.3
|
||||||
|
--------
|
||||||
|
* 支持多用户登录
|
||||||
|
* 增加用户权限控制
|
||||||
|
|
||||||
|
|
||||||
|
v1.2.2
|
||||||
|
--------
|
||||||
|
* 用户登录页增加图形验证码
|
||||||
|
* 支持从旧版本升级
|
||||||
|
* 任务批量开启、关闭、删除
|
||||||
|
* 调度器与任务节点支持HTTPS双向认证
|
||||||
|
* 修复任务列表页总记录数显示错误
|
||||||
|
|
||||||
v1.1
|
v1.1
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 14 KiB 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'
|
|
||||||
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' -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)
|
||||||
|
}
|
||||||
-170
@@ -1,170 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ouqiang/gocron/modules/app"
|
|
||||||
"github.com/ouqiang/gocron/routers"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
"gopkg.in/macaron.v1"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"github.com/ouqiang/gocron/modules/logger"
|
|
||||||
"github.com/ouqiang/gocron/service"
|
|
||||||
"github.com/ouqiang/gocron/models"
|
|
||||||
"github.com/ouqiang/gocron/modules/setting"
|
|
||||||
"time"
|
|
||||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// web服务器默认端口
|
|
||||||
const DefaultPort = 5920
|
|
||||||
|
|
||||||
|
|
||||||
var CmdWeb = cli.Command{
|
|
||||||
Name: "web",
|
|
||||||
Usage: "run web server",
|
|
||||||
Action: runWeb,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "host",
|
|
||||||
Value: "0.0.0.0",
|
|
||||||
Usage: "bind host",
|
|
||||||
},
|
|
||||||
cli.IntFlag{
|
|
||||||
Name: "port,p",
|
|
||||||
Value: DefaultPort,
|
|
||||||
Usage: "bind port",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "env,e",
|
|
||||||
Value: "prod",
|
|
||||||
Usage: "runtime environment, dev|test|prod",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWeb(ctx *cli.Context) {
|
|
||||||
// 设置运行环境
|
|
||||||
setEnvironment(ctx)
|
|
||||||
// 初始化应用
|
|
||||||
app.InitEnv()
|
|
||||||
// 初始化模块 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
|
|
||||||
|
|
||||||
models.Db = models.CreateDb()
|
|
||||||
|
|
||||||
// 初始化定时任务
|
|
||||||
serviceTask := new(service.Task)
|
|
||||||
serviceTask.Initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析端口
|
|
||||||
func parsePort(ctx *cli.Context) int {
|
|
||||||
var port int = DefaultPort
|
|
||||||
if ctx.IsSet("port") {
|
|
||||||
port = ctx.Int("port")
|
|
||||||
}
|
|
||||||
if port <= 0 || port >= 65535 {
|
|
||||||
port = DefaultPort
|
|
||||||
}
|
|
||||||
|
|
||||||
return port
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHost(ctx *cli.Context) string {
|
|
||||||
if ctx.IsSet("host") {
|
|
||||||
return ctx.String("host")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "0.0.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
func setEnvironment(ctx *cli.Context) {
|
|
||||||
var env string = "prod"
|
|
||||||
if ctx.IsSet("env") {
|
|
||||||
env = ctx.String("env")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch env {
|
|
||||||
case "test":
|
|
||||||
macaron.Env = macaron.TEST
|
|
||||||
case "dev":
|
|
||||||
macaron.Env = macaron.DEV
|
|
||||||
default:
|
|
||||||
macaron.Env = macaron.PROD
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 捕捉信号
|
|
||||||
func catchSignal() {
|
|
||||||
c := make(chan os.Signal)
|
|
||||||
// todo 配置热更新, windows 不支持 syscall.SIGUSR1, syscall.SIGUSR2
|
|
||||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
for {
|
|
||||||
s := <- c
|
|
||||||
logger.Info("收到信号 -- ", s)
|
|
||||||
switch s {
|
|
||||||
case syscall.SIGHUP:
|
|
||||||
logger.Info("收到终端断开信号, 忽略")
|
|
||||||
case syscall.SIGINT, syscall.SIGTERM:
|
|
||||||
shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 应用退出
|
|
||||||
func shutdown() {
|
|
||||||
defer func() {
|
|
||||||
logger.Info("已退出")
|
|
||||||
os.Exit(0)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if !app.Installed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("应用准备退出")
|
|
||||||
serviceTask := new(service.Task)
|
|
||||||
// 停止所有任务调度
|
|
||||||
logger.Info("停止定时任务调度")
|
|
||||||
serviceTask.StopAll()
|
|
||||||
|
|
||||||
taskNumInRunning := service.TaskNum.Num()
|
|
||||||
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
|
|
||||||
if taskNumInRunning > 0 {
|
|
||||||
logger.Info("等待所有任务执行完成后退出")
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
if taskNumInRunning <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
taskNumInRunning = service.TaskNum.Num()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放gRPC连接池
|
|
||||||
grpcpool.Pool.ReleaseAll()
|
|
||||||
}
|
|
||||||
@@ -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,37 +0,0 @@
|
|||||||
// +build node
|
|
||||||
// 任务节点
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ouqiang/gocron/modules/rpc/server"
|
|
||||||
"flag"
|
|
||||||
"runtime"
|
|
||||||
"os"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AppVersion = "1.1"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var serverAddr string
|
|
||||||
var allowRoot bool
|
|
||||||
var version bool
|
|
||||||
flag.BoolVar(&allowRoot, "allow-root", false, "./gocron-node -allow-root")
|
|
||||||
flag.StringVar(&serverAddr, "s", "0.0.0.0:5921", "./gocron-node -s ip:port")
|
|
||||||
flag.BoolVar(&version, "v", false, "./gocron-node -v")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if version {
|
|
||||||
fmt.Println(AppVersion)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS != "windows" && os.Getuid() == 0 && !allowRoot {
|
|
||||||
fmt.Println("Do not run gocron-node as root user")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
server.Start(serverAddr)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// +build gocron
|
|
||||||
// 调度中心
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ouqiang/gocron/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AppVersion = "1.1"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := cli.NewApp()
|
|
||||||
app.Name = "gocron"
|
|
||||||
app.Usage = "gocron service"
|
|
||||||
app.Version = AppVersion
|
|
||||||
app.Commands = []cli.Command{
|
|
||||||
cmd.CmdWeb,
|
|
||||||
}
|
|
||||||
app.Flags = append(app.Flags, []cli.Flag{}...)
|
|
||||||
app.Run(os.Args)
|
|
||||||
}
|
|
||||||
@@ -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 {
|
type TaskRequest struct {
|
||||||
Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
|
Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
|
||||||
Timeout int32 `protobuf:"varint,3,opt,name=timeout" json:"timeout,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{} }
|
func (m *TaskRequest) Reset() { *m = TaskRequest{} }
|
||||||
@@ -57,6 +58,13 @@ func (m *TaskRequest) GetTimeout() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *TaskRequest) GetId() int64 {
|
||||||
|
if m != nil {
|
||||||
|
return m.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type TaskResponse struct {
|
type TaskResponse struct {
|
||||||
Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"`
|
Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"`
|
||||||
Error string `protobuf:"bytes,2,opt,name=error" json:"error,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) }
|
func init() { proto.RegisterFile("task.proto", fileDescriptor0) }
|
||||||
|
|
||||||
var fileDescriptor0 = []byte{
|
var fileDescriptor0 = []byte{
|
||||||
// 170 bytes of a gzipped FileDescriptorProto
|
// 184 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xb1, 0x0e, 0x82, 0x40,
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xcf, 0x8a, 0x83, 0x30,
|
||||||
0x0c, 0x86, 0x45, 0x04, 0x63, 0x75, 0xd0, 0xc6, 0x98, 0x8b, 0x13, 0x61, 0x62, 0x30, 0x0c, 0xea,
|
0x10, 0xc6, 0x37, 0x46, 0x5d, 0x76, 0x76, 0x59, 0xda, 0xa1, 0x94, 0xd0, 0x93, 0x78, 0xf2, 0x50,
|
||||||
0xe8, 0xe2, 0x2b, 0x5c, 0x7c, 0x01, 0xc4, 0x1b, 0x0c, 0x81, 0x9e, 0xbd, 0xde, 0xfb, 0x1b, 0x38,
|
0x3c, 0xb4, 0x3d, 0xf6, 0x25, 0x1a, 0xfa, 0x02, 0x56, 0x73, 0x10, 0xd1, 0x49, 0x93, 0xc9, 0xfb,
|
||||||
0x48, 0x18, 0xbf, 0x36, 0xfd, 0xfa, 0xff, 0x00, 0x52, 0xb9, 0xa6, 0xb4, 0x4c, 0x42, 0x18, 0xb3,
|
0x17, 0xff, 0x81, 0xc7, 0xdf, 0x0c, 0xf3, 0xfd, 0xe6, 0x03, 0xe0, 0xca, 0x77, 0xa5, 0x75, 0xc4,
|
||||||
0xad, 0xf3, 0x27, 0x6c, 0x5f, 0x95, 0x6b, 0xb4, 0xf9, 0x79, 0xe3, 0x04, 0x15, 0xac, 0x6b, 0x6a,
|
0x84, 0xd2, 0xd9, 0x3a, 0x7f, 0xc0, 0xef, 0xb3, 0xf2, 0x9d, 0x36, 0xef, 0x60, 0x3c, 0xa3, 0x82,
|
||||||
0xdb, 0xaa, 0xfb, 0xa8, 0x65, 0x16, 0x15, 0x1b, 0x3d, 0x61, 0xbf, 0x91, 0x6f, 0x6b, 0xc8, 0x8b,
|
0xef, 0x9a, 0xfa, 0xbe, 0x1a, 0x1a, 0x15, 0x65, 0xa2, 0xf8, 0xd1, 0x2b, 0x8e, 0x1b, 0x6e, 0x7b,
|
||||||
0x8a, 0xb3, 0xa8, 0x48, 0xf4, 0x84, 0xf9, 0x03, 0x76, 0x41, 0xe1, 0x2c, 0x75, 0xce, 0xe0, 0x09,
|
0x43, 0x81, 0x95, 0xcc, 0x44, 0x91, 0xe8, 0x15, 0xf1, 0x1f, 0xa2, 0xb6, 0x51, 0x71, 0x26, 0x0a,
|
||||||
0x52, 0xf2, 0x62, 0xbd, 0xa8, 0x68, 0x50, 0x8c, 0x84, 0x47, 0x48, 0x0c, 0x33, 0xf1, 0x68, 0x0e,
|
0xa9, 0xa3, 0xb6, 0xc9, 0xef, 0xf0, 0x37, 0x47, 0x7a, 0x4b, 0x83, 0x37, 0x78, 0x84, 0x94, 0x02,
|
||||||
0x70, 0xbd, 0xc3, 0xaa, 0xbf, 0xc6, 0x0b, 0xc4, 0xda, 0x77, 0xb8, 0x2f, 0xd9, 0xd6, 0xe5, 0x2c,
|
0xdb, 0xc0, 0x4a, 0x4c, 0x91, 0x0b, 0xe1, 0x01, 0x12, 0xe3, 0x1c, 0xb9, 0xc5, 0x34, 0xc3, 0xe5,
|
||||||
0xd2, 0xf9, 0x30, 0x9b, 0x84, 0x0f, 0xf9, 0xe2, 0x9d, 0x0e, 0x15, 0x6e, 0xff, 0x00, 0x00, 0x00,
|
0x06, 0xf1, 0x78, 0x8d, 0x67, 0x90, 0x3a, 0x0c, 0xb8, 0x2b, 0x9d, 0xad, 0xcb, 0xcd, 0x8b, 0xa7,
|
||||||
0xff, 0xff, 0xef, 0x3c, 0x71, 0x6b, 0xd0, 0x00, 0x00, 0x00,
|
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 {
|
message TaskRequest {
|
||||||
string command = 2; // 命令
|
string command = 2; // 命令
|
||||||
int32 timeout = 3; // 任务执行超时时间
|
int32 timeout = 3; // 任务执行超时时间
|
||||||
|
int64 id = 4; // 执行任务唯一ID
|
||||||
}
|
}
|
||||||
|
|
||||||
message TaskResponse {
|
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,95 +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:"-"`
|
|
||||||
Selected bool `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, &TaskLog{}, &Host{}, setting,&LoginLog{},&TaskHost{},
|
|
||||||
}
|
|
||||||
for _, table := range tables {
|
|
||||||
exist, err:= Db.IsTableExist(table)
|
|
||||||
if exist {
|
|
||||||
return errors.New("数据表已存在")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = Db.Sync2(table)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setting.InitBasicField()
|
|
||||||
task.CreateTestTask()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建数据库
|
|
||||||
func isDatabaseExist(name string) bool {
|
|
||||||
_, err := Db.Exec("use ?", name)
|
|
||||||
|
|
||||||
return err != nil
|
|
||||||
}
|
|
||||||
-151
@@ -1,151 +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"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
go keepDbAlived(engine)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func keepDbAlived(engine *xorm.Engine) {
|
|
||||||
t := time.Tick(180 * time.Second)
|
|
||||||
for {
|
|
||||||
<- t
|
|
||||||
engine.Ping()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
-261
@@ -1,261 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
"github.com/go-xorm/xorm"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 Task struct {
|
|
||||||
Id int `xorm:"int pk autoincr"`
|
|
||||||
Name string `xorm:"varchar(32) notnull"` // 任务名称
|
|
||||||
Level TaskLevel `xorm:"smallint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
|
|
||||||
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
|
|
||||||
DependencyStatus TaskDependencyStatus `xorm:"smallint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
|
|
||||||
Spec string `xorm:"varchar(64) notnull"` // crontab
|
|
||||||
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:系统命令
|
|
||||||
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
|
|
||||||
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
|
||||||
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
|
|
||||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
|
|
||||||
NotifyStatus int8 `xorm:"smallint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
|
|
||||||
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
|
|
||||||
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
|
||||||
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
|
|
||||||
Status Status `xorm:"tinyint notnull index default 0"` // 状态 1:正常 0:停止
|
|
||||||
Created time.Time `xorm:"datetime notnull created"` // 创建时间
|
|
||||||
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
|
|
||||||
BaseModel `xorm:"-"`
|
|
||||||
Hosts []TaskHostDetail `xorm:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func taskHostTableName() []string {
|
|
||||||
return []string{TablePrefix + "task_host", "th"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增
|
|
||||||
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.Level = TaskLevelParent
|
|
||||||
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,remark,notify_status,notify_type,notify_receiver_id, dependency_task_id, dependency_status").
|
|
||||||
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() ([]Task, error) {
|
|
||||||
list := make([]Task, 0)
|
|
||||||
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
return session.GroupBy("t.id").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("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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
|
|
||||||
type TaskHost struct {
|
|
||||||
Id int `xorm:"int pk autoincr"`
|
|
||||||
TaskId int `xorm:"int not null index"`
|
|
||||||
HostId int16 `xorm:"smallint not null index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskHostDetail struct {
|
|
||||||
TaskHost `xorm:"extends"`
|
|
||||||
Name string
|
|
||||||
Port int
|
|
||||||
Alias string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (TaskHostDetail) TableName() string {
|
|
||||||
return TablePrefix + "task_host"
|
|
||||||
}
|
|
||||||
|
|
||||||
func hostTableName() []string {
|
|
||||||
return []string{TablePrefix + "host", "h"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (th *TaskHost) Remove(taskId int) error {
|
|
||||||
_, err := Db.Where("task_id = ?", taskId).Delete(new(TaskHost))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (th *TaskHost) Add(taskId int, hostIds []int) error {
|
|
||||||
|
|
||||||
err := th.Remove(taskId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
taskHosts := make([]TaskHost, len(hostIds))
|
|
||||||
for i, value := range hostIds {
|
|
||||||
taskHosts[i].TaskId = taskId
|
|
||||||
taskHosts[i].HostId = int16(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = Db.Insert(&taskHosts)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (th *TaskHost) GetHostIdsByTaskId(taskId int) ([]TaskHostDetail, error) {
|
|
||||||
list := make([]TaskHostDetail, 0)
|
|
||||||
fields := "th.id,th.host_id,h.alias,h.name,h.port"
|
|
||||||
err := Db.Alias("th").
|
|
||||||
Join("LEFT", hostTableName(), "th.host_id=h.id").
|
|
||||||
Where("th.task_id = ?", taskId).
|
|
||||||
Cols(fields).
|
|
||||||
Find(&list)
|
|
||||||
|
|
||||||
return list, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (th *TaskHost) GetTaskIdsByHostId(hostId int16) ([]interface{}, error) {
|
|
||||||
list := make([]TaskHost, 0)
|
|
||||||
err := Db.Where("host_id = ?", hostId).Cols("task_id").Find(&list)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
taskIds := make([]interface{}, len(list))
|
|
||||||
for i, value := range list {
|
|
||||||
taskIds[i] = value.TaskId
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskIds, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断主机id是否有引用
|
|
||||||
func (th *TaskHost) HostIdExist(hostId int16) (bool, error) {
|
|
||||||
count, err := Db.Where("host_id = ?", hostId).Count(th);
|
|
||||||
|
|
||||||
return count > 0, err
|
|
||||||
}
|
|
||||||
@@ -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,64 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ouqiang/gocron/modules/logger"
|
|
||||||
"github.com/ouqiang/gocron/modules/utils"
|
|
||||||
"gopkg.in/ini.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
AppDir string // 应用根目录
|
|
||||||
ConfDir string // 配置目录
|
|
||||||
LogDir string // 日志目录
|
|
||||||
DataDir string // 存放session等
|
|
||||||
AppConfig string // 应用配置文件
|
|
||||||
Installed bool // 应用是否安装过
|
|
||||||
Setting *ini.Section // 应用配置
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func InitEnv() {
|
|
||||||
logger.InitLogger()
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err)
|
|
||||||
}
|
|
||||||
AppDir = wd
|
|
||||||
ConfDir = AppDir + "/conf"
|
|
||||||
LogDir = AppDir + "/log"
|
|
||||||
DataDir = AppDir + "/data"
|
|
||||||
AppConfig = ConfDir + "/app.ini"
|
|
||||||
checkDirExists(ConfDir, LogDir, DataDir)
|
|
||||||
Installed = IsInstalled()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断应用是否安装过
|
|
||||||
func IsInstalled() bool {
|
|
||||||
_, err := os.Stat(ConfDir + "/install.lock")
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建安装锁文件
|
|
||||||
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 ExecWithRetry(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
|
|
||||||
tryTimes := 60
|
|
||||||
i := 0
|
|
||||||
for i < tryTimes {
|
|
||||||
output, err := Exec(ip, port, taskReq)
|
|
||||||
if err != errUnavailable {
|
|
||||||
return output, err
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, codes.Internal:
|
|
||||||
conn.Close()
|
|
||||||
*connClosed = true
|
|
||||||
return "", errUnavailable
|
|
||||||
case codes.DeadlineExceeded:
|
|
||||||
return "", errors.New("执行超时, 强制结束")
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
@@ -1,120 +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()
|
|
||||||
pool, ok := p.conns[addr]
|
|
||||||
p.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
err := p.newCommonPool(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.RLock()
|
|
||||||
pool = p.conns[addr]
|
|
||||||
p.RUnlock()
|
|
||||||
conn, err := pool.Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn.(*grpc.ClientConn), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GRPCPool) Put(addr string, conn *grpc.ClientConn) error {
|
|
||||||
p.RLock()
|
|
||||||
defer p.RUnlock()
|
|
||||||
pool, ok := p.conns[addr]
|
|
||||||
if ok {
|
|
||||||
return pool.Put(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrInvalidConn
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 释放连接池
|
|
||||||
func (p *GRPCPool) Release(addr string) {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
pool, ok := p.conns[addr]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pool.Release()
|
|
||||||
delete(p.conns, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放所有连接池
|
|
||||||
func (p *GRPCPool) ReleaseAll() {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
for _, pool := range(p.conns) {
|
|
||||||
pool.Release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化底层连接池
|
|
||||||
func (p *GRPCPool) newCommonPool(addr string) (error) {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
commonPool, ok := p.conns[addr]
|
|
||||||
if ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
poolConfig := &pool.PoolConfig{
|
|
||||||
InitialCap: 1,
|
|
||||||
MaxCap: 30,
|
|
||||||
Factory: func() (interface{}, error) {
|
|
||||||
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,50 +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) {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
grpclog.Println("panic", err)
|
|
||||||
}
|
|
||||||
} ()
|
|
||||||
l, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
grpclog.Fatal(err)
|
|
||||||
}
|
|
||||||
s := grpc.NewServer()
|
|
||||||
pb.RegisterTaskServer(s, Server{})
|
|
||||||
grpclog.Println("listen ", 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.
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