fix
This commit is contained in:
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# ---> Go
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
*.json
|
||||
client
|
||||
server
|
||||
.data
|
||||
data
|
||||
.idea
|
||||
|
||||
.directory
|
||||
.vscode
|
||||
|
||||
build
|
||||
binTmp
|
||||
dump.rdb
|
||||
app/jiacrontab_admin/jiacrontab_admin
|
||||
app/jiacrontabd/jiacrontabd
|
||||
|
||||
app/jiacrontabd/logs
|
||||
app/jiacrontab_admin/logs
|
||||
app/jiacrontabd/pprof
|
||||
app/jiacrontab_admin/pprof
|
||||
/node_modules
|
||||
|
||||
33
.travis.yml
Normal file
33
.travis.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.12.x
|
||||
|
||||
# Only clone the most recent commit.
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
# Skip the install step. Don't `go get` dependencies. Only build with the code
|
||||
# in vendor/
|
||||
install: true
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- go: 1.11.x
|
||||
env: GO111MODULE=on
|
||||
- go: 1.12.x
|
||||
env: GO111MODULE=on
|
||||
|
||||
services:
|
||||
- postgresql
|
||||
|
||||
# Don't email me the results of the test runs.
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
script:
|
||||
- make test
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
45
CHANGELOG.md
Normal file
45
CHANGELOG.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 更新记录
|
||||
|
||||
## v2.2.0
|
||||
1. 支持钉钉通知
|
||||
2. 修复mysql下无法自动创建表
|
||||
3. 修复无法正确删除动态
|
||||
4. 修复重启jiacrontabd时任务进程数量异常
|
||||
5. 修复编辑定时任务时无法正确停止原有任务
|
||||
|
||||
## v2.1.0
|
||||
1. 日志默认倒序查询
|
||||
2. 任务支持根据关键词搜索
|
||||
3. 新增磁盘清理动态清理
|
||||
4. 节点日志管理
|
||||
5. 修复bug
|
||||
|
||||
|
||||
## v2.0.5
|
||||
|
||||
1. 修复修改job后前一次调度计划不能马上停止
|
||||
2. 修复管理员审核时编辑普通用户job造成的普通用户job丢失
|
||||
3. 修复密码无法修改
|
||||
4. 新增不活跃节点列表
|
||||
|
||||
## v2.0.4
|
||||
|
||||
1. 修复常驻任务无法删除
|
||||
2. 修复查看日志在特殊情况下日志异常
|
||||
3. 新增修改分组
|
||||
|
||||
## v2.0.3
|
||||
|
||||
1. 修复特定情况下节点失去连接
|
||||
2. 修复定时任务隔天无法生成日志目录
|
||||
|
||||
## v2.0.2
|
||||
|
||||
1. 修复由于多次修改job造成的定时器重复调度
|
||||
|
||||
## v2.0.1
|
||||
|
||||
1. 修复进程数量显示异常
|
||||
2. 手动执行任务的支持kill
|
||||
3. 修复死锁造成的运行异常
|
||||
4. 修复依赖任务自定义代码不执行
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM yarnpkg/dev as frontend-env
|
||||
|
||||
WORKDIR /jiacrontab
|
||||
RUN apt-get install git
|
||||
RUN git clone https://github.com/jiacrontab/jiacrontab-frontend.git
|
||||
WORKDIR /jiacrontab/jiacrontab-frontend
|
||||
RUN yarn && yarn build
|
||||
|
||||
FROM golang AS jiacrontab-build
|
||||
WORKDIR /jiacrontab
|
||||
COPY . .
|
||||
COPY --from=frontend-env /jiacrontab/jiacrontab-frontend/build /jiacrontab/frontend-build
|
||||
RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct
|
||||
RUN GO111MODULE=on go get -u github.com/go-bindata/go-bindata/v3/go-bindata
|
||||
RUN make build assets=frontend-build
|
||||
|
||||
FROM debian AS jiarontab-run
|
||||
COPY --from=jiacrontab-build /jiacrontab/build /jiacrontab/build
|
||||
WORKDIR /jiacrontab/bin
|
||||
VOLUME ["/jiacrontab/bin/data"]
|
||||
EXPOSE 20001 20000 20003
|
||||
RUN mv /jiacrontab/build/jiacrontab/jiacrontabd/* . && mv /jiacrontab/build/jiacrontab/jiacrontab_admin/* .
|
||||
ENTRYPOINT []
|
||||
|
||||
72
LICENSE
Normal file
72
LICENSE
Normal file
@@ -0,0 +1,72 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
84
Makefile
Normal file
84
Makefile
Normal file
@@ -0,0 +1,84 @@
|
||||
# Go parameters
|
||||
goCmd=go
|
||||
version=$(shell cat VERSION)
|
||||
goBuild=$(goCmd) build -ldflags "-X jiacrontab/pkg/version.Binary=$(version)"
|
||||
goClean=$(goCmd) clean
|
||||
goTest=$(goCmd) test
|
||||
goGet=$(goCmd) get
|
||||
sourceAdmDir=./app/jiacrontab_admin
|
||||
sourceNodeDir=./app/jiacrontabd
|
||||
binAdm=$(sourceAdmDir)/jiacrontab_admin
|
||||
binNode=$(sourceNodeDir)/jiacrontabd
|
||||
|
||||
|
||||
buildDir=./build
|
||||
buildAdmDir=$(buildDir)/jiacrontab/jiacrontab_admin
|
||||
buildNodeDir=$(buildDir)/jiacrontab/jiacrontabd
|
||||
|
||||
admCfg=$(sourceAdmDir)/jiacrontab_admin.ini
|
||||
nodeCfg=$(sourceNodeDir)/jiacrontabd.ini
|
||||
staticDir=./jiacrontab_admin/static/build
|
||||
staticSourceDir=./jiacrontab_admin/static
|
||||
workDir=$(shell pwd)
|
||||
|
||||
|
||||
.PHONY: all build test clean build-linux build-windows
|
||||
all: test build
|
||||
build:
|
||||
$(call checkStatic)
|
||||
$(call init)
|
||||
$(goBuild) -o $(binAdm) -v $(sourceAdmDir)
|
||||
$(goBuild) -o $(binNode) -v $(sourceNodeDir)
|
||||
mv $(binAdm) $(buildAdmDir)
|
||||
mv $(binNode) $(buildNodeDir)
|
||||
build2:
|
||||
$(call init)
|
||||
$(goBuild) -o $(binAdm) -v $(sourceAdmDir)
|
||||
$(goBuild) -o $(binNode) -v $(sourceNodeDir)
|
||||
mv $(binAdm) $(buildAdmDir)
|
||||
mv $(binNode) $(buildNodeDir)
|
||||
docker:
|
||||
docker build \
|
||||
-t iwannay/jiacrontab:$(version) \
|
||||
-f Dockerfile \
|
||||
.
|
||||
test:
|
||||
$(goTest) -v -race -coverprofile=coverage.txt -covermode=atomic $(sourceAdmDir)
|
||||
$(goTest) -v -race -coverprofile=coverage.txt -covermode=atomic $(sourceNodeDir)
|
||||
clean:
|
||||
rm -f $(binAdm)
|
||||
rm -f $(binNode)
|
||||
rm -rf $(buildDir)
|
||||
|
||||
|
||||
# Cross compilation
|
||||
build-linux:
|
||||
$(call checkStatic)
|
||||
$(call init)
|
||||
GOOS=linux GOARCH=amd64 $(goBuild) -o $(binAdm) -v $(sourceAdmDir)
|
||||
GOOS=linux GOARCH=amd64 $(goBuild) -o $(binNode) -v $(sourceNodeDir)
|
||||
mv $(binAdm) $(buildAdmDir)
|
||||
mv $(binNode) $(buildNodeDir)
|
||||
|
||||
build-windows:
|
||||
$(call checkStatic)
|
||||
$(call init)
|
||||
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC="x86_64-w64-mingw32-gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -lssp" $(goBuild) -o $(binAdm).exe -v $(sourceAdmDir)
|
||||
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC="x86_64-w64-mingw32-gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -lssp" $(goBuild) -o $(binNode).exe -v $(sourceNodeDir)
|
||||
|
||||
mv $(binAdm).exe $(buildAdmDir)
|
||||
mv $(binNode).exe $(buildNodeDir)
|
||||
|
||||
define checkStatic
|
||||
@if [ "$(assets)" = "" ]; then echo "no assets, see https://github.com/jiacrontab/jiacrontab-frontend"; exit -1;else echo "build release"; fi
|
||||
go-bindata -pkg admin -prefix $(assets) -o jiacrontab_admin/bindata_gzip.go -fs $(assets)/...
|
||||
endef
|
||||
|
||||
define init
|
||||
rm -rf $(buildDir)
|
||||
mkdir $(buildDir)
|
||||
mkdir -p $(buildAdmDir)
|
||||
mkdir -p $(buildNodeDir)
|
||||
cp $(admCfg) $(buildAdmDir)
|
||||
cp $(nodeCfg) $(buildNodeDir)
|
||||
endef
|
||||
163
README.md
Normal file
163
README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
## jiacrontab
|
||||
|
||||
[](https://travis-ci.org/iwannay/jiacrontab)
|
||||
|
||||
简单可信赖的任务管理工具
|
||||
|
||||
### v2.0.0版发布
|
||||
|
||||
|
||||
### [❤jiacrontab 最新版下载点这里❤ ](https://jiacrontab.iwannay.cn/download/)
|
||||
|
||||
1.自定义job执行
|
||||
2.允许设置job的最大并发数
|
||||
3.每个脚本都可在web界面下灵活配置,如测试脚本运行,查看日志,强杀进程,停止定时...
|
||||
4.允许添加脚本依赖(支持跨服务器),依赖脚本提供同步和异步的执行模式
|
||||
5.支持异常通知
|
||||
6.支持守护脚本进程
|
||||
7.支持节点分组
|
||||
|
||||
|
||||
### 架构
|
||||
|
||||
<img src="https://raw.githubusercontent.com/iwannay/static_dir/master/jiacrontab_arch.png" width="50%"/>
|
||||
|
||||
### 说明
|
||||
|
||||
jiacrontab 由 jiacrontab_admin,jiacrontabd 两部分构成,两者完全独立通过 rpc 通信
|
||||
jiacrontab_admin:管理后台向用户提供web操作界面
|
||||
jiacrontabd:负责job数据存储,任务调度
|
||||
|
||||
|
||||
### 安装
|
||||
|
||||
#### 二进制安装
|
||||
|
||||
1.[下载](https://jiacrontab.iwannay.cn/download/) 二进制文件。
|
||||
|
||||
2.解压缩进入目录(jiarontab_admin,jiacrontabd)。
|
||||
|
||||
3.运行
|
||||
|
||||
```sh
|
||||
$ nohup ./jiacrontab_admin &> jiacrontab_admin.log &
|
||||
$ nohup ./jiacrontabd &> jiacrontabd.log &
|
||||
|
||||
## 建议使用systemd守护
|
||||
```
|
||||
|
||||
#### v2.x.x源码安装
|
||||
|
||||
1.安装 git,golang(version 1.12.x);可参考官网。
|
||||
2.安装运行
|
||||
|
||||
```sh
|
||||
$ git clone git@github.com:iwannay/jiacrontab.git
|
||||
$ cd jiacrontab
|
||||
# 配置代理
|
||||
$ go env -w GONOPROXY=\*\*.baidu.com\*\* ## 配置GONOPROXY环境变量,所有百度内代码,不走代理
|
||||
$ go env -w GONOSUMDB=\* ## 配置GONOSUMDB,暂不支持sumdb索引
|
||||
$ go env -w GOPROXY=https://goproxy.baidu.com ## 配置GOPROXY,可以下载墙外代码
|
||||
|
||||
# 编译
|
||||
# 注意需要先编译前端(https://github.com/jiacrontab/jiacrontab-frontend)
|
||||
# 再安装go-bindata
|
||||
# 然后assets指定前端资源编译后的位置
|
||||
$ GO111MODULE=on go get -u github.com/go-bindata/go-bindata/v3/go-bindata
|
||||
$ make build assets=$HOME/project/jiacrontab-frontend/build
|
||||
|
||||
$ cd build/jiacrontab/jiacrontab_admin/
|
||||
$ nohup ./jiacrontab_admin &> jiacrontab_admin.log &
|
||||
|
||||
$ cd build/jiacrontab/jiacrontabd/
|
||||
$ nohup ./jiacrontabd &> jiacrontabd.log &
|
||||
```
|
||||
|
||||
<font color="red" size="3">浏览器访问 host:port (eg: localhost:20000) 即可访问管理后台</font>
|
||||
|
||||
#### docker 安装
|
||||
```sh
|
||||
# 下载镜像
|
||||
$ docker pull iwannay/jiacrontab:2.3.0
|
||||
|
||||
# 创建自定义网络
|
||||
$ docker network create mybridge
|
||||
|
||||
# 启动jiacrontab_admin
|
||||
# 需要指定配置文件目录时需要先挂载目录,然后-config指定
|
||||
$ docker run --network mybridge --name jiacrontab_admin -p 20000:20000 -it iwannay/jiacrontab:2.3.0 ./jiacrontab_admin
|
||||
|
||||
# 启动jiacrontabd
|
||||
# 需要指定配置文件目录时需要先挂载目录,然后-config指定
|
||||
$ docker run -v $(pwd)/jiacrontabd:/config --name jiacrontabd --network mybridge -it iwannay/jiacrontab:2.3.0 ./jiacrontabd -config /config/jiacrontabd.ini
|
||||
|
||||
```
|
||||
|
||||
### 升级版本
|
||||
|
||||
1、下载新版本压缩包,并解压。
|
||||
|
||||
2、替换旧版jiacrontab_admin,jiacrontabd为新版执行文件
|
||||
|
||||
3、运行
|
||||
|
||||
### 基本使用
|
||||
|
||||
#### 定时任务
|
||||
|
||||
##### 超时设置和超时操作
|
||||
|
||||
超时后会进行设置的超时操作 默认值为 0 不判断超时
|
||||
|
||||
##### 最大并发数
|
||||
|
||||
最大并发数控制同一job同一个时刻最多允许存在的进程数,默认最大并发数为1,当前一次未执行结束时则放弃后续执行。
|
||||
防止脚本无法正常退出而导致系统资源耗尽
|
||||
|
||||
##### 添加依赖
|
||||
|
||||
依赖就是用户脚本执行前,需要先执行依赖脚本,只有依赖脚本执行完毕才会执行当前脚本。
|
||||
1. **并发执行**
|
||||
并发执行依赖脚本,任意一个脚本出错或超时不会影响其他依赖脚本,但是会中断用户job
|
||||
2. **同步执行**
|
||||
同步执行依赖脚本,执行顺序为添加顺序,如果有一个依赖脚本出错或超时,则会中断后继依赖,以及用户job
|
||||
|
||||
##### 脚本异常退出通知
|
||||
|
||||
如果脚本退出码不为0,则认为是异常退出
|
||||
|
||||
#### 常驻任务
|
||||
|
||||
常驻任务检查脚本进程是否退出,如果退出再次重启,保证脚本不停运行。
|
||||
注意:不支持后台进程。
|
||||
|
||||
## 附录
|
||||
|
||||
### 错误日志
|
||||
|
||||
错误日志存放在配置文件设置的目录下
|
||||
定时任务为 logs/crontab_task
|
||||
定时任务为 logs/daemon_task
|
||||
日志文件准确为日期目录下的 ID.log (eg: logs/crontab_task/2018/01/01/1.log)
|
||||
|
||||
#### 错误日志信息
|
||||
|
||||
1. 正常错误日志
|
||||
程序原因产生的错误日志
|
||||
2. 自定义错误日志
|
||||
程序中自定义输出的信息,需要在输出信息后面加入换行
|
||||
|
||||
### 截图
|
||||
<img src="https://raw.githubusercontent.com/iwannay/static_dir/master/jiacrontab_preview_2.0.0_1.png" width="50%"/><img src="https://raw.githubusercontent.com/iwannay/static_dir/master/jiacrontab_preview_2.0.0_2.png" width="50%" />
|
||||
|
||||
### 演示地址
|
||||
|
||||
[2.0.0版本演示地址](http://jiacrontab-spa.iwannay.cn/) 账号:test 密码:123456
|
||||
|
||||
### 商务合作
|
||||
<img src="https://raw.githubusercontent.com/iwannay/static_dir/master/1555767089.jpg" width="250" alt="合作"/>
|
||||
|
||||
|
||||
### 赞助
|
||||
本项目花费了作者大量时间,如果你觉的该项目对你有用,或者你希望该项目有更好的发展,欢迎赞助。
|
||||
<img src="https://raw.githubusercontent.com/iwannay/jiacrontab/master/admire.jpg" width="250" alt="赞助"/><img src="https://raw.githubusercontent.com/iwannay/jiacrontab/master/qq.png" width="250" alt="qq群"/>
|
||||
BIN
admire.jpg
Normal file
BIN
admire.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
44
app/jiacrontab_admin/jiacrontab_admin.ini
Normal file
44
app/jiacrontab_admin/jiacrontab_admin.ini
Normal file
@@ -0,0 +1,44 @@
|
||||
[app]
|
||||
http_listen_addr = 0.0.0.0:20000
|
||||
rpc_listen_addr = :20003
|
||||
app_name = jiacrontab
|
||||
; http 返回签名
|
||||
signing_key = `WERRTT1234$@#@@$`
|
||||
log_level = warn
|
||||
; 客户端最大心跳时间
|
||||
max_client_alive_interval = 30
|
||||
|
||||
[jwt]
|
||||
; jwt 签名
|
||||
signing_key = eyJhbGciOiJIUzI1
|
||||
expires = 3600
|
||||
name = token
|
||||
|
||||
[mail]
|
||||
enabled = true
|
||||
host = smtp.163.com:25
|
||||
user = jiacrontab@163.com
|
||||
skip_verify = true
|
||||
passwd = xxxxxx
|
||||
from = jiacrontab@163.com
|
||||
use_certificate = false
|
||||
|
||||
[ldap]
|
||||
; 支持: ldap://, ldaps://, ldapi://.
|
||||
addr = ladp://localhost:1234
|
||||
disabled_anonymous_query = false
|
||||
bind_passwd= 123456
|
||||
bind_userdn = "cn=admin,dc=jdevops,dc=com"
|
||||
basedn = "dc=jdevops,dc=com"
|
||||
user_field = uid
|
||||
|
||||
[database]
|
||||
; jiacrontab_admin目前支持的数据库包括sqlite3,mysql,pg
|
||||
; 注意: mysql,pg 等数据库需要手动建立jiacrontab库
|
||||
; driver_name = postgres
|
||||
; dsn = postgres://jiacrontab:123456@localhost:5432/jiacrontab?sslmode=disable
|
||||
; driver_name = mysql
|
||||
; dsn = root:12345678@(localhost:3306)/jiacrontab?charset=utf8&parseTime=True&loc=Local
|
||||
driver_name = sqlite3
|
||||
dsn = data/jiacrontab_admin.db?cache=shared
|
||||
|
||||
88
app/jiacrontab_admin/main.go
Normal file
88
app/jiacrontab_admin/main.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
admin "jiacrontab/jiacrontab_admin"
|
||||
"jiacrontab/pkg/pprof"
|
||||
"os"
|
||||
|
||||
"flag"
|
||||
|
||||
"jiacrontab/pkg/version"
|
||||
|
||||
"jiacrontab/pkg/util"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
var (
|
||||
debug bool
|
||||
cfgPath string
|
||||
logLevel string
|
||||
user string
|
||||
resetpwd bool
|
||||
pwd string
|
||||
)
|
||||
|
||||
func parseFlag(opt *admin.Config) *flag.FlagSet {
|
||||
flagSet := flag.NewFlagSet("jiacrontab_admin", flag.ExitOnError)
|
||||
// app options
|
||||
flagSet.Bool("version", false, "打印版本信息")
|
||||
flagSet.Bool("help", false, "帮助信息")
|
||||
flagSet.StringVar(&logLevel, "log_level", "warn", "日志级别(debug|info|warn|error)")
|
||||
flagSet.BoolVar(&debug, "debug", false, "开启debug模式")
|
||||
flagSet.StringVar(&cfgPath, "config", "./jiacrontab_admin.ini", "配置文件路径")
|
||||
flagSet.BoolVar(&resetpwd, "resetpwd", false, "重置密码")
|
||||
flagSet.StringVar(&pwd, "pwd", "", "重置密码时的新密码")
|
||||
flagSet.StringVar(&user, "user", "", "重置密码时的用户名")
|
||||
// jwt options
|
||||
flagSet.Parse(os.Args[1:])
|
||||
|
||||
if flagSet.Lookup("version").Value.(flag.Getter).Get().(bool) {
|
||||
fmt.Println(version.String("jiacrontab_admin"))
|
||||
os.Exit(0)
|
||||
}
|
||||
if flagSet.Lookup("help").Value.(flag.Getter).Get().(bool) {
|
||||
flagSet.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
opt.CfgPath = cfgPath
|
||||
|
||||
opt.Resolve()
|
||||
|
||||
if util.HasFlagName(flagSet, "debug") {
|
||||
opt.App.Debug = debug
|
||||
}
|
||||
|
||||
if util.HasFlagName(flagSet, "log_level") {
|
||||
opt.App.LogLevel = logLevel
|
||||
}
|
||||
if debug {
|
||||
log.JSON("debug config:", opt)
|
||||
}
|
||||
return flagSet
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := admin.NewConfig()
|
||||
parseFlag(cfg)
|
||||
log.SetLevel(map[string]int{
|
||||
"debug": 0,
|
||||
"info": 1,
|
||||
"warn": 2,
|
||||
"error": 3,
|
||||
}[cfg.App.LogLevel])
|
||||
pprof.ListenPprof()
|
||||
admin := admin.New(cfg)
|
||||
if resetpwd {
|
||||
if err := admin.ResetPwd(user, pwd); err != nil {
|
||||
fmt.Printf("failed reset passwrod (%s)\n", err)
|
||||
} else {
|
||||
fmt.Printf("reset password success!\n")
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
}
|
||||
admin.Main()
|
||||
}
|
||||
22
app/jiacrontabd/jiacrontabd.ini
Normal file
22
app/jiacrontabd/jiacrontabd.ini
Normal file
@@ -0,0 +1,22 @@
|
||||
[jiacrontabd]
|
||||
; 任务日志页面显示冗余信息,比如时间、脚本名称
|
||||
verbose_job_log = false
|
||||
; 本机rpc监听地址
|
||||
listen_addr = :20002
|
||||
; 当前节点的广播地址,admin通过该地址与当前节点通信,默认取当前节点ip
|
||||
; boardcast_addr = localhost:20001
|
||||
; admin 地址
|
||||
admin_addr = jiacrontab_admin:20003
|
||||
; 自动清理大于一个月或者单文件体积大于1G的日志文件
|
||||
auto_clean_task_log = true
|
||||
; 节点名,默认取节点hostname
|
||||
; node_name = node1
|
||||
log_level = warn
|
||||
log_path = ./logs
|
||||
user_agent = jiacrontabd
|
||||
; jiacrontabd目前仅支持sqlite3
|
||||
driver_name = sqlite3
|
||||
dsn = data/jiacrontabd.db?cache=shared
|
||||
|
||||
; 心跳上报周期(s)
|
||||
client_alive_interval = 10
|
||||
77
app/jiacrontabd/main.go
Normal file
77
app/jiacrontabd/main.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"jiacrontab/jiacrontabd"
|
||||
"jiacrontab/pkg/pprof"
|
||||
"jiacrontab/pkg/util"
|
||||
"jiacrontab/pkg/version"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
func parseFlag(opt *jiacrontabd.Config) *flag.FlagSet {
|
||||
|
||||
var (
|
||||
debug bool
|
||||
cfgPath string
|
||||
logLevel string
|
||||
boardcastAddr string
|
||||
)
|
||||
|
||||
flagSet := flag.NewFlagSet("jiacrontabd", flag.ExitOnError)
|
||||
flagSet.Bool("version", false, "打印版本信息")
|
||||
flagSet.Bool("help", false, "帮助信息")
|
||||
flagSet.StringVar(&logLevel, "log_level", "warn", "日志级别(debug|info|warn|error)")
|
||||
flagSet.BoolVar(&debug, "debug", false, "开启debug模式")
|
||||
flagSet.StringVar(&boardcastAddr, "boardcast_addr", "", fmt.Sprintf("广播地址(default: %s:20001)", util.InternalIP()))
|
||||
flagSet.StringVar(&cfgPath, "config", "./jiacrontabd.ini", "配置文件路径")
|
||||
flagSet.Parse(os.Args[1:])
|
||||
if flagSet.Lookup("version").Value.(flag.Getter).Get().(bool) {
|
||||
fmt.Println(version.String("jiacrontab_admin"))
|
||||
os.Exit(0)
|
||||
}
|
||||
if flagSet.Lookup("help").Value.(flag.Getter).Get().(bool) {
|
||||
flagSet.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
opt.CfgPath = cfgPath
|
||||
opt.Resolve()
|
||||
|
||||
// TODO: can be better
|
||||
if util.HasFlagName(flagSet, "log_level") {
|
||||
opt.LogLevel = logLevel
|
||||
}
|
||||
|
||||
if util.HasFlagName(flagSet, "debug") {
|
||||
opt.Debug = debug
|
||||
}
|
||||
|
||||
if util.HasFlagName(flagSet, "boardcast_addr") {
|
||||
opt.BoardcastAddr = boardcastAddr
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.JSON("debug config:", opt)
|
||||
}
|
||||
|
||||
return flagSet
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := jiacrontabd.NewConfig()
|
||||
parseFlag(cfg)
|
||||
log.SetLevel(map[string]int{
|
||||
"debug": 0,
|
||||
"info": 1,
|
||||
"warn": 2,
|
||||
"error": 3,
|
||||
}[cfg.LogLevel])
|
||||
pprof.ListenPprof()
|
||||
jiad := jiacrontabd.New(cfg)
|
||||
jiad.Main()
|
||||
}
|
||||
21
deployment/jiacrontab_admin.service
Normal file
21
deployment/jiacrontab_admin.service
Normal file
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=jiacrontab_admin service
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
ProtectSystem=full
|
||||
WorkingDirectory=/opt/jiacrontab/jiacrontab_admin
|
||||
ExecStart=/opt/jiacrontab/jiacrontab_admin/jiacrontab_admin
|
||||
KillMode=process
|
||||
KillSignal=SIGTERM
|
||||
SendSIGKILL=no
|
||||
Restart=on-abort
|
||||
RestartSec=5s
|
||||
UMask=007
|
||||
262
deployment/jiacrontabctl
Normal file
262
deployment/jiacrontabctl
Normal file
@@ -0,0 +1,262 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------
|
||||
# The initial version was released by Vipkwd
|
||||
# -----------------------------------------------------------------
|
||||
# 应用场景:本脚本需配合jiacrontab v2.2.0+ 版本使用
|
||||
# (https://jiacrontab.iwannay.cn/download/jiacrontab-v2.2.0-linux-amd64.zip)
|
||||
#
|
||||
# 部署方法:1、解压上述zip二进制包; 2、将本脚本放入解压后的目录终呈如下结构
|
||||
#
|
||||
# [root@vipkwd jiacrontab]# pwd
|
||||
# /data/wwwroot/jiacrontab
|
||||
# [root@vipkwd jiacrontab]# ll
|
||||
# 总用量 16
|
||||
# 1033992 -rwx--x--x 1 root root 7.4K 8月 14 21:30 jiacrontabctl
|
||||
# 1034047 drwxr-xr-x 3 root www 4.0K 8月 11 08:27 jiacrontab_admin
|
||||
# 1033995 drwxr-xr-x 4 root www 4.0K 8月 11 14:24 jiacrontabd
|
||||
# [root@vipkwd_com jiacrontab]#
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
|
||||
# -----配置项 start-----
|
||||
# 提供进程搜索关键字(即: "jiacrontab_admin"与“jiacrontabd” 在公共字符串部分,用于shell grep 过滤)
|
||||
APP_KEYWORDS=jiacrontab
|
||||
|
||||
# 提供web前端服务的脚本文件(目录)名(对应: ./jiacrontab_admin/jiacrontab_admin)
|
||||
ADMIN_SCRIPT_NAME=jiacrontab_admin
|
||||
|
||||
# 任务调度服务脚本文件(目录)名(对应: ./jiacrontabd/jiacrontabd)
|
||||
BD_SCRIPT_NAME=jiacrontabd
|
||||
|
||||
#手动指定项目PATH(默认空,自动定位 pwd 指令目录)
|
||||
DEPLOY_PATH=
|
||||
# -----配置项 end-----
|
||||
|
||||
|
||||
FORMATER_LINE="------------------------------------------------------------------------------------------------------"
|
||||
ECHO_PREFIX=" -- "
|
||||
STIME_LOGFILE=".stime.log"
|
||||
|
||||
# 绝对定位工作目录
|
||||
PROJECT_ROOT=${DEPLOY_PATH:=`pwd`}
|
||||
if [ ! -d $PROJECT_ROOT ];then
|
||||
echo -e "[\033[31mError\033[0m] DEPLOY_PATH \033[33m${DEPLOY_PATH}\033[0m is not a directory!"
|
||||
exit 1
|
||||
fi
|
||||
ai=0
|
||||
apps=("${ADMIN_SCRIPT_NAME}" "${BD_SCRIPT_NAME}")
|
||||
for app in ${apps[*]}
|
||||
do
|
||||
path=${PROJECT_ROOT}/${app}
|
||||
if [ ! -d $path ];then
|
||||
echo -e "[\033[31mError\033[0m] Project path: \033[33m${path}\033[0m is not a directory!"
|
||||
ai=$(($ai+1))
|
||||
fi
|
||||
done
|
||||
|
||||
apps=""
|
||||
|
||||
[[ $ai -gt 0 ]] && exit 1
|
||||
|
||||
ADMIN_CLI_IS_RUNNING=0
|
||||
BD_CLI_IS_RUNNING=0
|
||||
|
||||
function _timeNow(){
|
||||
nowtime=`date "+%Y-%m-%d %H:%M:%S"`
|
||||
}
|
||||
|
||||
function _runApp(){
|
||||
if [ "${1}" == "0" ];then
|
||||
path=${PROJECT_ROOT}/${2}
|
||||
if [ ! -d $path ];then
|
||||
echo -e "[\033[31mError\033[0m] \033[33m${path}\033[0m is not directory(Skiped)!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "${path}/${2}" ];then
|
||||
echo -e "[\033[31mError\033[0m] Service script \033[33m${path}/${2}\033[0m is not exist(Skiped)!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cd $path
|
||||
`nohup ./${2} &> ${2}.log &`
|
||||
#sleep 1;
|
||||
_timeNow
|
||||
echo "${2}[ StartTime: ${nowtime} ]" >> ${PROJECT_ROOT}/${STIME_LOGFILE}
|
||||
echo -e " ${ECHO_PREFIX} \033[33m${2}\033[0m (\033[32mStarted\033[0m)"
|
||||
else
|
||||
echo -e " ${ECHO_PREFIX} \033[33m${2:=Unknow}\033[0m is running (\033[33mSkiped\033[0m)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function _cliListen(){
|
||||
for cli in `ps -ef | grep "$APP_KEYWORDS" | grep -v "grep" | awk -F ' ./' '{print $2}'`
|
||||
do
|
||||
if [ "${cli}" == "${ADMIN_SCRIPT_NAME}" ];then
|
||||
ADMIN_CLI_IS_RUNNING=1
|
||||
fi
|
||||
if [ "${cli}" == "${BD_SCRIPT_NAME}" ];then
|
||||
BD_CLI_IS_RUNNING=1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function _parseDuringTime(){
|
||||
#parseTimeResultStr=""
|
||||
logfile="${PROJECT_ROOT}/${STIME_LOGFILE}"
|
||||
if [ -f $logfile ];then
|
||||
stime=`grep "${1}\[ StartTime:" ${logfile} | tail -1 | awk '{print $3 " "$4}'`
|
||||
_timeNow
|
||||
time1=$(($(date +%s -d "$nowtime") - $(date +%s -d "$stime")));
|
||||
array=("Duration: ")
|
||||
config=(31579200 "Year" 2635200 "Month" 86400 "Day" 3600 "Hour" 60 "Minute" 1 "Second")
|
||||
j=0
|
||||
for str in ${config[*]}; do
|
||||
if [ $(($j%2)) -eq 0 ];then
|
||||
if [ ${time1} -ge ${config[$j]} ];then
|
||||
j1=$(($j+1))
|
||||
j2=$(($j+2))
|
||||
array[$j1]=$[ $time1 / ${config[$j]} ]
|
||||
array[$j2]=${config[$j1]}
|
||||
if [ ${array[$j1]} -gt 1 ];then
|
||||
array[$j2]="${array[$j2]}s"
|
||||
fi
|
||||
time1=$(($time1 % ${config[$j]}))
|
||||
fi
|
||||
fi
|
||||
j=$(($j+1))
|
||||
done
|
||||
if [ "Null" == "${stime}Null" ];then
|
||||
return 0
|
||||
fi
|
||||
parseTimeResultStr="${stime} (${array[*]})"
|
||||
return 1
|
||||
fi
|
||||
parseTimeResultStr=""
|
||||
return 0
|
||||
}
|
||||
|
||||
function start(){
|
||||
_cliListen
|
||||
if [ $ADMIN_CLI_IS_RUNNING -eq 0 -a $BD_CLI_IS_RUNNING -eq 0 ];then
|
||||
echo "" > ${PROJECT_ROOT}/${STIME_LOGFILE}
|
||||
fi
|
||||
_runApp "${ADMIN_CLI_IS_RUNNING}" "${ADMIN_SCRIPT_NAME}"
|
||||
_runApp "${BD_CLI_IS_RUNNING}" "${BD_SCRIPT_NAME}"
|
||||
}
|
||||
|
||||
function stop(){
|
||||
for line in `ps -ef | grep "$APP_KEYWORDS" | grep -v grep | awk -F ' ' '{print $2 " " $NF}'`
|
||||
do
|
||||
STR=`echo $line | awk '{print $1}'`
|
||||
if [ $STR -gt 0 ] 2> /dev/null ;then
|
||||
kill -9 $STR;
|
||||
else
|
||||
echo -e " ${ECHO_PREFIX} \033[33m${STR}\033[0m (\033[34mStoped\033[0m) "
|
||||
#sleep 1;
|
||||
fi
|
||||
done
|
||||
echo "" > ${PROJECT_ROOT}/${STIME_LOGFILE}
|
||||
}
|
||||
|
||||
function status(){
|
||||
_cliListen
|
||||
echo $FORMATER_LINE
|
||||
echo " ${ECHO_PREFIX}"
|
||||
if [ $ADMIN_CLI_IS_RUNNING -eq 0 ];then
|
||||
echo -e " ${ECHO_PREFIX} Active: \033[31minactive (dead)\033[0m \033[33m${ADMIN_SCRIPT_NAME}\033[0m since Unknow"
|
||||
else
|
||||
_parseDuringTime "${ADMIN_SCRIPT_NAME}"
|
||||
if [ $? -gt 0 ] 2> /dev/null;then
|
||||
# 全局获取函数返回的字符串
|
||||
echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${ADMIN_SCRIPT_NAME}\033[0m since ${parseTimeResultStr}"
|
||||
#sleep 1
|
||||
else
|
||||
echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${ADMIN_SCRIPT_NAME}\033[0m since Unknow"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $BD_CLI_IS_RUNNING -eq 0 ];then
|
||||
echo -e " ${ECHO_PREFIX} Active: \033[31minactive (dead)\033[0m \033[33m${BD_SCRIPT_NAME}\033[0m since Unknow"
|
||||
else
|
||||
_parseDuringTime "${BD_SCRIPT_NAME}"
|
||||
if [ $? -gt 0 ] 2> /dev/null;then
|
||||
# 全局获取函数返回的字符串
|
||||
echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${BD_SCRIPT_NAME}\033[0m since ${parseTimeResultStr}"
|
||||
#sleep 1
|
||||
else
|
||||
echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${BD_SCRIPT_NAME}\033[0m since Unknow"
|
||||
fi
|
||||
fi
|
||||
echo " ${ECHO_PREFIX}"
|
||||
echo $FORMATER_LINE
|
||||
if [ $ADMIN_CLI_IS_RUNNING -gt 0 -o $BD_CLI_IS_RUNNING -gt 0 ];then
|
||||
|
||||
ps -ef | grep "$APP_KEYWORDS" | grep -v grep
|
||||
sleep 1
|
||||
echo $FORMATER_LINE
|
||||
netstat -tlpan | grep "$APP_KEYWORDS" | grep -v grep
|
||||
fi
|
||||
}
|
||||
|
||||
function cli(){
|
||||
echo "input: $0 OPTION" >&2
|
||||
echo " OPTION :"
|
||||
echo " start"
|
||||
echo " stop"
|
||||
echo " status"
|
||||
echo " restart"
|
||||
echo " h | help"
|
||||
}
|
||||
|
||||
function helpDoc() {
|
||||
echo $FORMATER_LINE
|
||||
#echo " "
|
||||
echo " Jiacrontab 简单可信赖的任务管理工具(V2.2.0)"
|
||||
echo " 1.自定义job执行"
|
||||
echo " 2.允许设置job的最大并发数"
|
||||
echo " 3.每个脚本都可在web界面下灵活配置,如测试脚本运行,查看日志,强杀进程,停止定时..."
|
||||
echo " 4.允许添加脚本依赖(支持跨服务器),依赖脚本提供同步和异步的执行模式"
|
||||
echo " 5.支持异常通知"
|
||||
echo " 6.支持守护脚本进程"
|
||||
echo " 7.支持节点分组"
|
||||
echo " n.更多请访问仓库查看"
|
||||
#echo " "
|
||||
echo $FORMATER_LINE
|
||||
#echo " "
|
||||
echo " Github: https://github.com/iwannay/jiacrontab"
|
||||
echo " Csdn: https://codechina.csdn.net/mirrors/iwannay/jiacrontab"
|
||||
echo " "
|
||||
echo " Package: https://jiacrontab.iwannay.cn/download/"
|
||||
#echo " "
|
||||
echo $FORMATER_LINE
|
||||
cli
|
||||
}
|
||||
COMMAND=$1
|
||||
shift 1
|
||||
case $COMMAND in
|
||||
start)
|
||||
start;
|
||||
;;
|
||||
stop)
|
||||
stop;
|
||||
;;
|
||||
status)
|
||||
status;
|
||||
;;
|
||||
restart)
|
||||
stop;
|
||||
sleep 1
|
||||
start;
|
||||
sleep 1
|
||||
status;
|
||||
;;
|
||||
h|help)
|
||||
helpDoc;
|
||||
;;
|
||||
*)
|
||||
cli;
|
||||
exit 1;
|
||||
;;
|
||||
esac
|
||||
20
deployment/jiacrontabd.service
Normal file
20
deployment/jiacrontabd.service
Normal file
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=jiacrontabd service
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
ProtectSystem=full
|
||||
WorkingDirectory=/opt/jiacrontab/jiacrontabd
|
||||
ExecStart=/opt/jiacrontab/jiacrontabd/jiacrontabd
|
||||
KillMode=process
|
||||
KillSignal=SIGTERM
|
||||
SendSIGKILL=no
|
||||
Restart=on-abort
|
||||
RestartSec=5s
|
||||
UMask=007
|
||||
37
go.mod
Normal file
37
go.mod
Normal file
@@ -0,0 +1,37 @@
|
||||
module jiacrontab
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Joker/hpp v1.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/google/go-cmp v0.5.1 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect
|
||||
github.com/iris-contrib/middleware/cors v0.0.0-20200810001613-32cf668f999f
|
||||
github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f
|
||||
github.com/iwannay/log v0.0.0-20190630100042-7fa98f256ca1
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||
github.com/kataras/iris/v12 v12.1.9-0.20200814111841-d0d7679a98f2
|
||||
github.com/lib/pq v1.8.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/onsi/ginkgo v1.10.1 // indirect
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
github.com/smartystreets/assertions v1.0.1 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.58.0
|
||||
gorm.io/driver/mysql v1.0.3
|
||||
gorm.io/driver/postgres v1.0.5
|
||||
gorm.io/driver/sqlite v1.1.3
|
||||
gorm.io/gorm v1.20.6
|
||||
)
|
||||
491
go.sum
Normal file
491
go.sum
Normal file
@@ -0,0 +1,491 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v4 v4.1.0 h1:EvZLdRZYZ6iljADEhrfWSCSNOJRDntNqEJJmeP0Sjg0=
|
||||
github.com/CloudyKit/jet/v4 v4.1.0/go.mod h1:DhUsGNEpjPmBD0zmGNP8DaSV1dGO8g9U4adIK8BCWmw=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.1-0.20200619015827-c3da72aa01ed h1:G/gj6aolvcaqMTCmlHRDsLLQlJ/fXTC4vE9o18KRZtw=
|
||||
github.com/andybalholm/brotli v1.0.1-0.20200619015827-c3da72aa01ed/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
|
||||
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
|
||||
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/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
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/dgraph-io/badger/v2 v2.0.3/go.mod h1:3KY8+bsP8wI0OEnQJAKpd4wIJW/Mm32yw2j/9FUVnIM=
|
||||
github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
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/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ=
|
||||
github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.3/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2-0.20200519141726-cb32006e483f h1:qa1wFcvZzVLbFVPdsdTsWL6k5IP6BEmFmd9SeahRQ5s=
|
||||
github.com/google/uuid v1.1.2-0.20200519141726-cb32006e483f/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f h1:KMlcu9X58lhTA/KrfX8Bi1LQSO4pzoVjTiL3h4Jk+Zk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/httpexpect/v2 v2.0.5 h1:b2Orx2FXRhnmZil4td66C8zzkHnssSoFQP2HQtyktJg=
|
||||
github.com/iris-contrib/httpexpect/v2 v2.0.5/go.mod h1:JpRu+DEVVCA6KHLKUAs72QoaevQESqLHuG5s1CQ+QiA=
|
||||
github.com/iris-contrib/jade v1.1.4 h1:WoYdfyJFfZIUgqNAeOyRfTNQZOksSlZ6+FnXR3AEpX0=
|
||||
github.com/iris-contrib/jade v1.1.4/go.mod h1:EDqR+ur9piDl6DUgs6qRrlfzmlx/D5UybogqrXvJTBE=
|
||||
github.com/iris-contrib/middleware/cors v0.0.0-20200810001613-32cf668f999f h1:twQZd21/tSQPaAuJKuYLKvNyB/lw1dIe2kWz9RDvxSs=
|
||||
github.com/iris-contrib/middleware/cors v0.0.0-20200810001613-32cf668f999f/go.mod h1:dmEl2AW/sx5szUpqERCyD0uJBcCVZrbWZFBhvqHnEeQ=
|
||||
github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f h1:4PJtwZeyY7jqigFhO7PuNiyD2joElia2Nn6+BXi/dps=
|
||||
github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f/go.mod h1:CXIqdYkSJA3XCO1fAD6bHZozgH9CrGNCHmrdHI6hkk4=
|
||||
github.com/iris-contrib/pongo2 v0.0.1 h1:zGP7pW51oi5eQZMIlGA3I+FHY9/HOQWDB+572yin0to=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.2 h1:qd3RU2sLPaTTamv6BGn+PDD5Gmny+i3jO8xDAmWnNLs=
|
||||
github.com/iris-contrib/schema v0.0.2/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
|
||||
github.com/iwannay/log v0.0.0-20190630100042-7fa98f256ca1 h1:3CFmSUt7+8NHOo7S1nVDy+WpDXCBUNBJJOGx0sjndgA=
|
||||
github.com/iwannay/log v0.0.0-20190630100042-7fa98f256ca1/go.mod h1:uZNVAfgxWNr2WSlZaPFV4nd8pLH3qDWD/A498RmicAI=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.7.0 h1:pwjzcYyfmz/HQOQlENvG1OcDqauTGaqlVahq934F0/U=
|
||||
github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.5 h1:NUbEWPmCQZbMmYlTjVoNPhc0CfnYyz2bfUAh6A5ZVJM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.5.0 h1:jzBqRk2HFG2CV4AIwgCI2PwTgm6UUoCAK2ofHHRirtc=
|
||||
github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.9.0 h1:6STjDqppM2ROy5p1wNDcsC7zJTjSHeuCsguZmXyzx7c=
|
||||
github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/blocks v0.0.2 h1:XSeCznI3UW8MJZj2rH2lQRVrsyahdJzagBcQf7N8OeE=
|
||||
github.com/kataras/blocks v0.0.2/go.mod h1:KPyOYc1M3MgzsznVcdjErtcYWO3AZXQbQ8fMYWcr3oA=
|
||||
github.com/kataras/golog v0.0.18/go.mod h1:jRYl7dFYqP8aQj9VkwdBUXYZSfUktm+YYg1arJILfyw=
|
||||
github.com/kataras/golog v0.1.0 h1:7MCQeWkC18dn1Y431E85sUCalTLrhYrFGhLnHo29/QI=
|
||||
github.com/kataras/golog v0.1.0/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA=
|
||||
github.com/kataras/iris/v12 v12.1.9-0.20200809192844-da029d6f3722/go.mod h1:cTaBLtn3jPr4QuZ9uF/o/GyWzpVfQLsk6uJpLLUUp8M=
|
||||
github.com/kataras/iris/v12 v12.1.9-0.20200814111841-d0d7679a98f2 h1:JRJIgvyzB6Fwdi9YaFtK/Kt5zpaHYYdHdmBwsSmShn4=
|
||||
github.com/kataras/iris/v12 v12.1.9-0.20200814111841-d0d7679a98f2/go.mod h1:gx7fL2wqb3bPpxMNoIII6R4dKxI9SZapItl0rS8F7Jo=
|
||||
github.com/kataras/neffos v0.0.16/go.mod h1:BqWkF1c6cSyqw85dfCdqXxK5cMo/hyBGhtNuFkxHyMg=
|
||||
github.com/kataras/pio v0.0.8/go.mod h1:NFfMp2kVP1rmV4N6gH6qgWpuoDKlrOeYi3VrAIWCGsE=
|
||||
github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g=
|
||||
github.com/kataras/pio v0.0.10/go.mod h1:gS3ui9xSD+lAUpbYnjOGiQyY7sUMJO+EHpiRzhtZ5no=
|
||||
github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kataras/tunnel v0.0.1 h1:VsphzEnN8es/0tw7lwxC94pHwUa60nE9SYojKhbWjME=
|
||||
github.com/kataras/tunnel v0.0.1/go.mod h1:Gslr6f+Y0esb704OqMi8hNOfFImgXcmg4KiN2zScgvs=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.10.11 h1:K9z59aO18Aywg2b/WSgBaUX99mHy2BES18Cr5lBKZHk=
|
||||
github.com/klauspost/compress v1.10.11/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
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.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mediocregopher/radix/v3 v3.5.0/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/mediocregopher/radix/v3 v3.5.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats.go v1.9.2/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693/go.mod h1:6hSY48PjDm4UObWmGLyJE9DxYVKTgR9kbCspXXJEhcU=
|
||||
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/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk=
|
||||
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI=
|
||||
github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
|
||||
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 h1:yi1hN8dcqI9l8klZfy4B8mJvFmmAxJEePIQQFNSd7Cs=
|
||||
golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/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=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
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/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.58.0 h1:VdDvTzv/005R8vEFyQ56bpEnOKTNPbpJhL0VCohxlQw=
|
||||
gopkg.in/ini.v1 v1.58.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
|
||||
gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
|
||||
gorm.io/driver/postgres v1.0.5 h1:raX6ezL/ciUmaYTvOq48jq1GE95aMC0CmxQYbxQ4Ufw=
|
||||
gorm.io/driver/postgres v1.0.5/go.mod h1:qrD92UurYzNctBMVCJ8C3VQEjffEuphycXtxOudXNCA=
|
||||
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
|
||||
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
||||
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.6 h1:qa7tC1WcU+DBI/ZKMxvXy1FcrlGsvxlaKufHrT2qQ08=
|
||||
gorm.io/gorm v1.20.6/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
|
||||
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
|
||||
1
jiacrontab_admin/.gitignore
vendored
Normal file
1
jiacrontab_admin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bindata_gzip.go
|
||||
92
jiacrontab_admin/admin.go
Normal file
92
jiacrontab_admin/admin.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/mailer"
|
||||
"jiacrontab/pkg/rpc"
|
||||
"time"
|
||||
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type Admin struct {
|
||||
cfg atomic.Value
|
||||
ldap *Ldap
|
||||
initAdminUser int32
|
||||
}
|
||||
|
||||
func (n *Admin) getOpts() *Config {
|
||||
return n.cfg.Load().(*Config)
|
||||
}
|
||||
|
||||
func (n *Admin) swapOpts(opts *Config) {
|
||||
n.cfg.Store(opts)
|
||||
}
|
||||
|
||||
func New(opt *Config) *Admin {
|
||||
adm := &Admin{}
|
||||
adm.swapOpts(opt)
|
||||
return adm
|
||||
}
|
||||
|
||||
func (a *Admin) init() {
|
||||
cfg := a.getOpts()
|
||||
if err := models.InitModel(cfg.Database.DriverName, cfg.Database.DSN, cfg.App.Debug); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if models.DB().Take(&models.User{}, "group_id=?", 1).Error == nil {
|
||||
atomic.StoreInt32(&a.initAdminUser, 1)
|
||||
}
|
||||
// mail
|
||||
if cfg.Mailer.Enabled {
|
||||
mailer.InitMailer(&mailer.Mailer{
|
||||
QueueLength: cfg.Mailer.QueueLength,
|
||||
SubjectPrefix: cfg.Mailer.SubjectPrefix,
|
||||
From: cfg.Mailer.From,
|
||||
Host: cfg.Mailer.Host,
|
||||
User: cfg.Mailer.User,
|
||||
Passwd: cfg.Mailer.Passwd,
|
||||
FromEmail: cfg.Mailer.FromEmail,
|
||||
DisableHelo: cfg.Mailer.DisableHelo,
|
||||
HeloHostname: cfg.Mailer.HeloHostname,
|
||||
SkipVerify: cfg.Mailer.SkipVerify,
|
||||
UseCertificate: cfg.Mailer.UseCertificate,
|
||||
CertFile: cfg.Mailer.CertFile,
|
||||
KeyFile: cfg.Mailer.KeyFile,
|
||||
UsePlainText: cfg.Mailer.UsePlainText,
|
||||
HookMode: false,
|
||||
})
|
||||
}
|
||||
a.ldap = &Ldap{
|
||||
BindUserDn: cfg.Ldap.BindUserdn,
|
||||
BindPwd: cfg.Ldap.BindPasswd,
|
||||
BaseOn: cfg.Ldap.Basedn,
|
||||
UserField: cfg.Ldap.UserField,
|
||||
Addr: cfg.Ldap.Addr,
|
||||
DisabledAnonymousQuery: cfg.Ldap.DisabledAnonymousQuery,
|
||||
Timeout: time.Second * time.Duration(cfg.Ldap.Timeout),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Admin) ResetPwd(username string, password string) error {
|
||||
if username == "" || password == "" {
|
||||
return errors.New("username or password cannot empty")
|
||||
}
|
||||
a.init()
|
||||
user := models.User{
|
||||
Username: username,
|
||||
Passwd: password,
|
||||
}
|
||||
return user.Update()
|
||||
}
|
||||
|
||||
func (a *Admin) Main() {
|
||||
cfg := a.getOpts()
|
||||
a.init()
|
||||
go rpc.ListenAndServe(cfg.App.RPCListenAddr, NewSrv(a))
|
||||
app := newApp(a)
|
||||
app.Run(iris.Addr(cfg.App.HTTPListenAddr))
|
||||
}
|
||||
172
jiacrontab_admin/app.go
Normal file
172
jiacrontab_admin/app.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/logger"
|
||||
|
||||
"jiacrontab/models"
|
||||
|
||||
"fmt"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/iris-contrib/middleware/cors"
|
||||
jwtmiddleware "github.com/iris-contrib/middleware/jwt"
|
||||
"github.com/kataras/iris/v12/context"
|
||||
)
|
||||
|
||||
func newApp(adm *Admin) *iris.Application {
|
||||
|
||||
app := iris.New()
|
||||
app.UseGlobal(newRecover(adm))
|
||||
app.Logger().SetLevel(adm.getOpts().App.LogLevel)
|
||||
app.Use(logger.New())
|
||||
app.HandleDir("/", AssetFile(), iris.DirOptions{
|
||||
IndexName: "index.html",
|
||||
Cache: iris.DirCacheOptions{
|
||||
Enable: true,
|
||||
CompressIgnore: iris.MatchImagesAssets,
|
||||
Encodings: []string{"gzip", "deflate", "br", "snappy"},
|
||||
CompressMinSize: 50,
|
||||
Verbose: 1,
|
||||
},
|
||||
})
|
||||
// app.StaticEmbeddedGzip("/", "./assets/", GzipAsset, GzipAssetNames)
|
||||
cfg := adm.getOpts()
|
||||
|
||||
wrapHandler := func(h func(ctx *myctx)) context.Handler {
|
||||
return func(c iris.Context) {
|
||||
h(wrapCtx(c, adm))
|
||||
}
|
||||
}
|
||||
|
||||
jwtHandler := jwtmiddleware.New(jwtmiddleware.Config{
|
||||
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(cfg.Jwt.SigningKey), nil
|
||||
},
|
||||
|
||||
Extractor: func(ctx iris.Context) (string, error) {
|
||||
token, err := url.QueryUnescape(ctx.GetHeader(cfg.Jwt.Name))
|
||||
return token, err
|
||||
},
|
||||
Expiration: true,
|
||||
|
||||
ErrorHandler: func(c iris.Context, err error) {
|
||||
ctx := wrapCtx(c, adm)
|
||||
if ctx.RequestPath(true) != "/user/login" {
|
||||
ctx.respAuthFailed(fmt.Errorf("Token verification failed(%s)", err))
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
},
|
||||
|
||||
SigningMethod: jwt.SigningMethodHS256,
|
||||
})
|
||||
|
||||
crs := cors.New(cors.Options{
|
||||
Debug: false,
|
||||
AllowedHeaders: []string{"Content-Type", "Token"},
|
||||
AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts.
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
app.Use(crs)
|
||||
app.AllowMethods(iris.MethodOptions)
|
||||
|
||||
v1 := app.Party("/v1")
|
||||
{
|
||||
v1.Post("/user/login", wrapHandler(Login))
|
||||
v1.Post("/app/init", wrapHandler(InitApp))
|
||||
}
|
||||
|
||||
v2 := app.Party("/v2")
|
||||
{
|
||||
v2.Use(jwtHandler.Serve)
|
||||
v2.Use(wrapHandler(func(ctx *myctx) {
|
||||
if err := ctx.parseClaimsFromToken(); err != nil {
|
||||
ctx.respJWTError(err)
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}))
|
||||
v2.Post("/crontab/job/list", wrapHandler(GetJobList))
|
||||
v2.Post("/crontab/job/get", wrapHandler(GetJob))
|
||||
v2.Post("/crontab/job/log", wrapHandler(GetRecentLog))
|
||||
v2.Post("/crontab/job/edit", wrapHandler(EditJob))
|
||||
v2.Post("/crontab/job/action", wrapHandler(ActionTask))
|
||||
v2.Post("/crontab/job/exec", wrapHandler(ExecTask))
|
||||
|
||||
v2.Post("/config/get", wrapHandler(GetConfig))
|
||||
v2.Post("/config/mail/send", wrapHandler(SendTestMail))
|
||||
v2.Post("/system/info", wrapHandler(SystemInfo))
|
||||
v2.Post("/log/info", wrapHandler(LogInfo))
|
||||
v2.Post("/log/clean", wrapHandler(CleanLog))
|
||||
|
||||
v2.Post("/daemon/job/list", wrapHandler(GetDaemonJobList))
|
||||
v2.Post("/daemon/job/action", wrapHandler(ActionDaemonTask))
|
||||
v2.Post("/daemon/job/edit", wrapHandler(EditDaemonJob))
|
||||
v2.Post("/daemon/job/get", wrapHandler(GetDaemonJob))
|
||||
v2.Post("/daemon/job/log", wrapHandler(GetRecentDaemonLog))
|
||||
|
||||
v2.Post("/group/list", wrapHandler(GetGroupList))
|
||||
v2.Post("/group/edit", wrapHandler(EditGroup))
|
||||
|
||||
v2.Post("/node/list", wrapHandler(GetNodeList))
|
||||
v2.Post("/node/delete", wrapHandler(DeleteNode))
|
||||
v2.Post("/node/group_node", wrapHandler(GroupNode))
|
||||
v2.Post("/node/clean_log", wrapHandler(CleanNodeLog))
|
||||
|
||||
v2.Post("/user/activity_list", wrapHandler(GetActivityList))
|
||||
v2.Post("/user/job_history", wrapHandler(GetJobHistory))
|
||||
v2.Post("/user/audit_job", wrapHandler(AuditJob))
|
||||
v2.Post("/user/stat", wrapHandler(UserStat))
|
||||
v2.Post("/user/signup", wrapHandler(Signup))
|
||||
v2.Post("/user/edit", wrapHandler(EditUser))
|
||||
v2.Post("/user/delete", wrapHandler(DeleteUser))
|
||||
v2.Post("/user/group_user", wrapHandler(GroupUser))
|
||||
v2.Post("/user/list", wrapHandler(GetUserList))
|
||||
}
|
||||
|
||||
debug := app.Party("/debug")
|
||||
{
|
||||
debug.Get("/stat", wrapHandler(stat))
|
||||
debug.Get("/pprof/", wrapHandler(indexDebug))
|
||||
debug.Get("/pprof/{key:string}", wrapHandler(pprofHandler))
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// InitApp 初始化应用
|
||||
func InitApp(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
user models.User
|
||||
reqBody InitAppReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if ret := models.DB().Take(&user, "group_id=?", 1); ret.Error == nil && ret.RowsAffected > 0 {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
user.Username = reqBody.Username
|
||||
user.Passwd = reqBody.Passwd
|
||||
user.Root = true
|
||||
user.GroupID = models.SuperGroup.ID
|
||||
user.Mail = reqBody.Mail
|
||||
|
||||
if err = user.Create(); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
atomic.StoreInt32(&ctx.adm.initAdminUser, 1)
|
||||
ctx.respSucc("", true)
|
||||
}
|
||||
216
jiacrontab_admin/config.go
Normal file
216
jiacrontab_admin/config.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"jiacrontab/pkg/file"
|
||||
"jiacrontab/pkg/mailer"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
appname = "jiacrontab"
|
||||
)
|
||||
|
||||
type AppOpt struct {
|
||||
HTTPListenAddr string `opt:"http_listen_addr"`
|
||||
RPCListenAddr string `opt:"rpc_listen_addr"`
|
||||
AppName string `opt:"app_name" `
|
||||
Debug bool `opt:"debug" `
|
||||
LogLevel string `opt:"log_level"`
|
||||
SigningKey string `opt:"signing_key"`
|
||||
MaxClientAliveInterval int `opt:"max_client_alive_interval"`
|
||||
}
|
||||
|
||||
type JwtOpt struct {
|
||||
SigningKey string `opt:"signing_key"`
|
||||
Name string `opt:"name" `
|
||||
Expires int64 `opt:"expires"`
|
||||
}
|
||||
|
||||
type MailerOpt struct {
|
||||
Enabled bool `opt:"enabled"`
|
||||
QueueLength int `opt:"queue_length"`
|
||||
SubjectPrefix string `opt:"subject_Prefix"`
|
||||
Host string `opt:"host"`
|
||||
From string `opt:"from"`
|
||||
FromEmail string `opt:"from_email"`
|
||||
User string `opt:"user"`
|
||||
Passwd string `opt:"passwd"`
|
||||
DisableHelo bool `opt:"disable_helo"`
|
||||
HeloHostname string `opt:"helo_hostname"`
|
||||
SkipVerify bool `opt:"skip_verify"`
|
||||
UseCertificate bool `opt:"use_certificate"`
|
||||
CertFile string `opt:"cert_file"`
|
||||
KeyFile string `opt:"key_file"`
|
||||
UsePlainText bool `opt:"use_plain_text"`
|
||||
}
|
||||
|
||||
type databaseOpt struct {
|
||||
DriverName string `opt:"driver_name"`
|
||||
DSN string `opt:"dsn"`
|
||||
}
|
||||
|
||||
type ldapOpt struct {
|
||||
Addr string `opt:"addr"`
|
||||
DisabledAnonymousQuery bool `opt:"disabled_anonymous_query"`
|
||||
Basedn string `opt:"basedn"`
|
||||
Timeout int `opt:"timeout"`
|
||||
BindPasswd string `opt:"bind_passwd"`
|
||||
BindUserdn string `opt:"bind_userdn"`
|
||||
UserField string `opt:"user_field"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mailer *MailerOpt `section:"mail"`
|
||||
Jwt *JwtOpt `section:"jwt"`
|
||||
App *AppOpt `section:"app"`
|
||||
Database *databaseOpt `section:"database"`
|
||||
Ldap *ldapOpt `section:"ldap"`
|
||||
|
||||
CfgPath string
|
||||
iniFile *ini.File
|
||||
ServerStartTime time.Time `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Config) Resolve() {
|
||||
|
||||
c.ServerStartTime = time.Now()
|
||||
c.iniFile = c.loadConfig(c.CfgPath)
|
||||
|
||||
val := reflect.ValueOf(c).Elem()
|
||||
typ := val.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
section := field.Tag.Get("section")
|
||||
if section == "" {
|
||||
continue
|
||||
}
|
||||
subVal := reflect.ValueOf(val.Field(i).Interface()).Elem()
|
||||
subtyp := subVal.Type()
|
||||
for j := 0; j < subtyp.NumField(); j++ {
|
||||
subField := subtyp.Field(j)
|
||||
subOpt := subField.Tag.Get("opt")
|
||||
if subOpt == "" {
|
||||
continue
|
||||
}
|
||||
sec := c.iniFile.Section(section)
|
||||
|
||||
if !sec.HasKey(subOpt) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := sec.Key(subOpt)
|
||||
|
||||
switch subField.Type.Kind() {
|
||||
case reflect.Bool:
|
||||
v, err := key.Bool()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
subVal.Field(j).SetBool(v)
|
||||
case reflect.String:
|
||||
subVal.Field(j).SetString(key.String())
|
||||
case reflect.Int64:
|
||||
v, err := key.Int64()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
subVal.Field(j).SetInt(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
App: &AppOpt{
|
||||
Debug: false,
|
||||
HTTPListenAddr: ":20000",
|
||||
RPCListenAddr: ":20003",
|
||||
AppName: "jiacrontab",
|
||||
LogLevel: "warn",
|
||||
SigningKey: "WERRTT1234$@#@@$",
|
||||
MaxClientAliveInterval: 30,
|
||||
},
|
||||
Mailer: &MailerOpt{
|
||||
Enabled: false,
|
||||
QueueLength: 1000,
|
||||
SubjectPrefix: "jiacrontab",
|
||||
SkipVerify: true,
|
||||
UseCertificate: false,
|
||||
},
|
||||
Jwt: &JwtOpt{
|
||||
SigningKey: "ADSFdfs2342$@@#",
|
||||
Name: "token",
|
||||
Expires: 3600,
|
||||
},
|
||||
Ldap: &ldapOpt{},
|
||||
Database: &databaseOpt{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) loadConfig(path string) *ini.File {
|
||||
if !file.Exist(path) {
|
||||
f, err := file.CreateFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
iniFile, err := ini.Load(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return iniFile
|
||||
}
|
||||
|
||||
func GetConfig(ctx *myctx) {
|
||||
cfg := ctx.adm.getOpts()
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"mail": map[string]interface{}{
|
||||
"host": cfg.Mailer.Host,
|
||||
"user": cfg.Mailer.User,
|
||||
"use_certificate": cfg.Mailer.UseCertificate,
|
||||
"skip_verify": cfg.Mailer.SkipVerify,
|
||||
"cert_file": cfg.Mailer.CertFile,
|
||||
"key_file": cfg.Mailer.KeyFile,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func SendTestMail(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody SendTestMailReqParams
|
||||
cfg = ctx.adm.getOpts()
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.Mailer.Enabled {
|
||||
err = mailer.SendMail([]string{reqBody.MailTo}, "jiacrontab欢迎你", "来自jiacrontab的温馨祝福!")
|
||||
if err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
ctx.respSucc("", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respBasicError(errors.New("邮箱服务未开启"))
|
||||
}
|
||||
32
jiacrontab_admin/const.go
Normal file
32
jiacrontab_admin/const.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package admin
|
||||
|
||||
const (
|
||||
event_DelNodeDesc = "{username}删除了节点{targetName}"
|
||||
event_RenameNode = "{username}将{sourceName}重命名为{targetName}"
|
||||
|
||||
event_EditCronJob = "{sourceName}{username}编辑了定时任务{targetName}"
|
||||
event_DelCronJob = "{sourceName}{username}删除了定时任务{targetName}"
|
||||
event_StopCronJob = "{sourceName}{username}停止了定时任务{targetName}"
|
||||
event_StartCronJob = "{sourceName}{username}启动了定时任务{targetName}"
|
||||
event_ExecCronJob = "{sourceName}{username}执行了定时任务{targetName}"
|
||||
event_KillCronJob = "{sourceName}{username}kill了定时任务进程{targetName}"
|
||||
|
||||
event_EditDaemonJob = "{sourceName}{username}编辑了常驻任务{targetName}"
|
||||
event_DelDaemonJob = "{sourceName}{username}删除了常驻任务{targetName}"
|
||||
event_StartDaemonJob = "{sourceName}{username}启动了常驻任务{targetName}"
|
||||
event_StopDaemonJob = "{sourceName}{username}停止了常驻任务{targetName}"
|
||||
|
||||
event_EditGroup = "{username}编辑了{targetName}组"
|
||||
event_GroupNode = "{username}将节点{sourceName}添加到{targetName}组"
|
||||
|
||||
event_SignUpUser = "{username}创建了用户{targetName}"
|
||||
event_EditUser = "{username}更新了用户信息"
|
||||
event_DeleteUser = "{username}删除了用户{targetName}"
|
||||
event_GroupUser = "{username}将用户{sourceUsername}设置为{targetName}组"
|
||||
event_AuditCrontabJob = "{sourceName}{username}审核了定时任务{targetName}"
|
||||
event_AuditDaemonJob = "{sourceName}{username}审核了常驻任务{targetName}"
|
||||
|
||||
event_CleanJobHistory = "{username}清除了{targetName}前的任务执行记录"
|
||||
event_CleanUserEvent = "{username}清除了{targetName}前的用户动态"
|
||||
event_CleanNodeLog = "{sourceName}{username}清除了{targetName}前的job动态"
|
||||
)
|
||||
266
jiacrontab_admin/crontab.go
Normal file
266
jiacrontab_admin/crontab.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetJobList(ctx *myctx) {
|
||||
|
||||
var (
|
||||
jobRet proto.QueryCrontabJobRet
|
||||
err error
|
||||
reqBody GetJobListReqParams
|
||||
rpcReqParams proto.QueryJobArgs
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
rpcReqParams.Page = reqBody.Page
|
||||
rpcReqParams.Pagesize = reqBody.Pagesize
|
||||
rpcReqParams.UserID = ctx.claims.UserID
|
||||
rpcReqParams.Root = ctx.claims.Root
|
||||
rpcReqParams.GroupID = ctx.claims.GroupID
|
||||
rpcReqParams.SearchTxt = reqBody.SearchTxt
|
||||
|
||||
if err := rpcCall(reqBody.Addr, "CrontabJob.List", rpcReqParams, &jobRet); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": jobRet.List,
|
||||
"page": jobRet.Page,
|
||||
"pagesize": jobRet.Pagesize,
|
||||
"total": jobRet.Total,
|
||||
})
|
||||
}
|
||||
|
||||
func GetRecentLog(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
searchRet proto.SearchLogResult
|
||||
reqBody GetLogReqParams
|
||||
logList []string
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "CrontabJob.Log", proto.SearchLog{
|
||||
JobID: reqBody.JobID,
|
||||
Offset: reqBody.Offset,
|
||||
Pagesize: reqBody.Pagesize,
|
||||
Date: reqBody.Date,
|
||||
Root: ctx.claims.Root,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
UserID: ctx.claims.UserID,
|
||||
Pattern: reqBody.Pattern,
|
||||
IsTail: reqBody.IsTail,
|
||||
}, &searchRet); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logList = strings.Split(string(searchRet.Content), "\n")
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"logList": logList,
|
||||
"curAddr": reqBody.Addr,
|
||||
"offset": searchRet.Offset,
|
||||
"filesize": searchRet.FileSize,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
|
||||
func EditJob(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reply models.CrontabJob
|
||||
reqBody EditJobReqParams
|
||||
job models.CrontabJob
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.verifyNodePermission(reqBody.Addr) {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
job = models.CrontabJob{
|
||||
Name: reqBody.Name,
|
||||
Command: reqBody.Command,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
Code: reqBody.Code,
|
||||
TimeArgs: models.TimeArgs{
|
||||
Month: reqBody.Month,
|
||||
Day: reqBody.Day,
|
||||
Hour: reqBody.Hour,
|
||||
Minute: reqBody.Minute,
|
||||
Weekday: reqBody.Weekday,
|
||||
Second: reqBody.Second,
|
||||
},
|
||||
|
||||
UpdatedUserID: ctx.claims.UserID,
|
||||
UpdatedUsername: ctx.claims.Username,
|
||||
WorkDir: reqBody.WorkDir,
|
||||
WorkUser: reqBody.WorkUser,
|
||||
WorkIp: reqBody.WorkIp,
|
||||
WorkEnv: reqBody.WorkEnv,
|
||||
KillChildProcess: reqBody.KillChildProcess,
|
||||
RetryNum: reqBody.RetryNum,
|
||||
Timeout: reqBody.Timeout,
|
||||
TimeoutTrigger: reqBody.TimeoutTrigger,
|
||||
MailTo: reqBody.MailTo,
|
||||
APITo: reqBody.APITo,
|
||||
DingdingTo: reqBody.DingdingTo,
|
||||
MaxConcurrent: reqBody.MaxConcurrent,
|
||||
DependJobs: reqBody.DependJobs,
|
||||
ErrorMailNotify: reqBody.ErrorMailNotify,
|
||||
ErrorAPINotify: reqBody.ErrorAPINotify,
|
||||
ErrorDingdingNotify: reqBody.ErrorDingdingNotify,
|
||||
IsSync: reqBody.IsSync,
|
||||
CreatedUserID: ctx.claims.UserID,
|
||||
CreatedUsername: ctx.claims.Username,
|
||||
}
|
||||
|
||||
job.ID = reqBody.JobID
|
||||
if ctx.claims.Root || ctx.claims.GroupID == models.SuperGroup.ID {
|
||||
job.Status = models.StatusJobOk
|
||||
} else {
|
||||
job.Status = models.StatusJobUnaudited
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "CrontabJob.Edit", proto.EditCrontabJobArgs{
|
||||
Job: job,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
Root: ctx.claims.Root,
|
||||
UserID: ctx.claims.UserID,
|
||||
}, &reply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
ctx.pubEvent(reply.Name, event_EditCronJob, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
ctx.respSucc("", reply)
|
||||
}
|
||||
|
||||
func ActionTask(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
method string
|
||||
reqBody ActionTaskReqParams
|
||||
jobReply []models.CrontabJob
|
||||
methods = map[string]string{
|
||||
"start": "CrontabJob.Start",
|
||||
"stop": "CrontabJob.Stop",
|
||||
"delete": "CrontabJob.Delete",
|
||||
"batch-exec": "CrontabJob.Execs",
|
||||
"kill": "CrontabJob.Kill",
|
||||
}
|
||||
|
||||
eDesc = map[string]string{
|
||||
"start": event_StartCronJob,
|
||||
"stop": event_StopCronJob,
|
||||
"batch-exec": event_ExecCronJob,
|
||||
"delete": event_DelCronJob,
|
||||
"kill": event_KillCronJob,
|
||||
}
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if method, ok = methods[reqBody.Action]; !ok {
|
||||
ctx.respBasicError(errors.New("action无法识别"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, method, proto.ActionJobsArgs{
|
||||
UserID: ctx.claims.UserID,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
Root: ctx.claims.Root,
|
||||
JobIDs: reqBody.JobIDs,
|
||||
}, &jobReply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
if len(jobReply) > 0 {
|
||||
var targetNames []string
|
||||
for _, v := range jobReply {
|
||||
targetNames = append(targetNames, v.Name)
|
||||
}
|
||||
ctx.pubEvent(strings.Join(targetNames, ","), eDesc[reqBody.Action], models.EventSourceName(reqBody.Addr), reqBody)
|
||||
}
|
||||
ctx.respSucc("", jobReply)
|
||||
}
|
||||
|
||||
func ExecTask(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
logList []string
|
||||
execJobReply proto.ExecCrontabJobReply
|
||||
reqBody JobReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "CrontabJob.Exec", proto.GetJobArgs{
|
||||
UserID: ctx.claims.UserID,
|
||||
Root: ctx.claims.Root,
|
||||
JobID: reqBody.JobID,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
}, &execJobReply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
logList = strings.Split(string(execJobReply.Content), "\n")
|
||||
ctx.pubEvent(execJobReply.Job.Name, event_ExecCronJob, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
ctx.respSucc("", logList)
|
||||
}
|
||||
|
||||
func GetJob(ctx *myctx) {
|
||||
var (
|
||||
reqBody GetJobReqParams
|
||||
crontabJob models.CrontabJob
|
||||
err error
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.verifyNodePermission(reqBody.Addr) {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "CrontabJob.Get", proto.GetJobArgs{
|
||||
UserID: ctx.claims.UserID,
|
||||
Root: ctx.claims.Root,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
JobID: reqBody.JobID,
|
||||
}, &crontabJob); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", crontabJob)
|
||||
}
|
||||
253
jiacrontab_admin/ctx.go
Normal file
253
jiacrontab_admin/ctx.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"jiacrontab/pkg/version"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type myctx struct {
|
||||
iris.Context
|
||||
adm *Admin
|
||||
claims CustomerClaims
|
||||
}
|
||||
|
||||
func wrapCtx(ctx iris.Context, adm *Admin) *myctx {
|
||||
key := "__ctx__"
|
||||
if v := ctx.Values().Get(key); v != nil {
|
||||
return v.(*myctx)
|
||||
}
|
||||
|
||||
c := &myctx{
|
||||
Context: ctx,
|
||||
adm: adm,
|
||||
}
|
||||
if atomic.LoadInt32(&adm.initAdminUser) == 1 {
|
||||
ctx.SetCookieKV("ready", "true", func(ctx iris.Context, c *http.Cookie, op uint8) {
|
||||
if op == 1 {
|
||||
c.HttpOnly = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ctx.SetCookieKV("ready", "false", func(ctx iris.Context, c *http.Cookie, op uint8) {
|
||||
if op == 1 {
|
||||
c.HttpOnly = false
|
||||
}
|
||||
})
|
||||
}
|
||||
ctx.Values().Set(key, c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (ctx *myctx) respNotAllowed() {
|
||||
ctx.respError(proto.Code_NotAllowed, proto.Msg_NotAllowed)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respAuthFailed(err error) {
|
||||
ctx.respError(proto.Code_FailedAuth, err)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respDBError(err error) {
|
||||
ctx.respError(proto.Code_DBError, err)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respJWTError(err error) {
|
||||
ctx.respError(proto.Code_JWTError, err)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respBasicError(err error) {
|
||||
ctx.respError(proto.Code_Error, err)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respParamError(err error) {
|
||||
ctx.respError(proto.Code_ParamsError, err)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respRPCError(err error) {
|
||||
ctx.respError(proto.Code_RPCError, err)
|
||||
}
|
||||
|
||||
func (ctx *myctx) respError(code int, err interface{}, v ...interface{}) {
|
||||
|
||||
var (
|
||||
sign string
|
||||
bts []byte
|
||||
msgStr string
|
||||
data interface{}
|
||||
cfg = ctx.adm.getOpts()
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
msgStr = "error"
|
||||
}
|
||||
msgStr = fmt.Sprintf("%s", err)
|
||||
if len(v) >= 1 {
|
||||
data = v[0]
|
||||
}
|
||||
|
||||
if strings.Contains(msgStr, "UNIQUE constraint failed") {
|
||||
msgStr = strings.Replace(msgStr, "UNIQUE constraint failed", "数据已存在,请检查索引", -1)
|
||||
}
|
||||
|
||||
bts, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Error("errorResp:", err)
|
||||
}
|
||||
|
||||
sign = fmt.Sprintf("%x", md5.Sum(append(bts, []byte(cfg.App.SigningKey)...)))
|
||||
|
||||
ctx.JSON(proto.Resp{
|
||||
Code: code,
|
||||
Msg: msgStr,
|
||||
Data: json.RawMessage(bts),
|
||||
Sign: sign,
|
||||
Version: version.String(cfg.App.AppName),
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *myctx) respSucc(msg string, v interface{}) {
|
||||
|
||||
cfg := ctx.adm.getOpts()
|
||||
if msg == "" {
|
||||
msg = "success"
|
||||
}
|
||||
|
||||
bts, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
log.Error("errorResp:", err)
|
||||
}
|
||||
|
||||
sign := fmt.Sprintf("%x", md5.Sum(append(bts, []byte(cfg.App.SigningKey)...)))
|
||||
|
||||
ctx.JSON(proto.Resp{
|
||||
Code: proto.SuccessRespCode,
|
||||
Msg: msg,
|
||||
Data: json.RawMessage(bts),
|
||||
Sign: sign,
|
||||
Version: version.String(cfg.App.AppName),
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *myctx) isSuper() bool {
|
||||
return ctx.claims.GroupID == models.SuperGroup.ID
|
||||
}
|
||||
func (ctx *myctx) isRoot() bool {
|
||||
return ctx.claims.Root
|
||||
}
|
||||
|
||||
func (ctx *myctx) parseClaimsFromToken() error {
|
||||
var ok bool
|
||||
|
||||
if (ctx.claims != CustomerClaims{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
token, ok := ctx.Values().Get("jwt").(*jwt.Token)
|
||||
if !ok {
|
||||
return errors.New("claims is nil")
|
||||
}
|
||||
bts, err := json.Marshal(token.Claims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(bts, &ctx.claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal claims error - (%s)", err)
|
||||
}
|
||||
var user models.User
|
||||
if err := models.DB().Take(&user, "id=?", ctx.claims.UserID).Error; err != nil {
|
||||
return fmt.Errorf("validate user from db error - (%s)", err)
|
||||
}
|
||||
if ctx.claims.Mail != user.Mail || ctx.claims.GroupID != user.GroupID || ctx.claims.Root != user.Root || ctx.claims.Version != user.Version {
|
||||
return fmt.Errorf("token validate error")
|
||||
}
|
||||
|
||||
if ctx.claims.GroupID == models.SuperGroup.ID {
|
||||
ctx.claims.Root = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *myctx) getGroupNodes() ([]models.Node, error) {
|
||||
var nodes []models.Node
|
||||
err := models.DB().Find(&nodes, "group_id=?", ctx.claims.GroupID).Error
|
||||
return nodes, err
|
||||
}
|
||||
|
||||
func (ctx *myctx) verifyNodePermission(addr string) bool {
|
||||
var node models.Node
|
||||
return node.VerifyUserGroup(ctx.claims.UserID, ctx.claims.GroupID, addr)
|
||||
}
|
||||
|
||||
func (ctx *myctx) getGroupAddr() ([]string, error) {
|
||||
var addrs []string
|
||||
nodes, err := ctx.getGroupNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, v := range nodes {
|
||||
addrs = append(addrs, v.Addr)
|
||||
}
|
||||
return addrs, nil
|
||||
|
||||
}
|
||||
|
||||
func (ctx *myctx) Valid(i Parameter) error {
|
||||
if err := ctx.ReadJSON(i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := i.Verify(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validStructRule(i); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *myctx) pubEvent(targetName, desc string, source interface{}, v interface{}) {
|
||||
var content string
|
||||
|
||||
if v != nil {
|
||||
bts, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
content = string(bts)
|
||||
}
|
||||
|
||||
e := models.Event{
|
||||
GroupID: ctx.claims.GroupID,
|
||||
UserID: ctx.claims.UserID,
|
||||
Username: ctx.claims.Username,
|
||||
EventDesc: desc,
|
||||
TargetName: targetName,
|
||||
Content: content,
|
||||
}
|
||||
|
||||
switch v := source.(type) {
|
||||
case models.EventSourceName:
|
||||
e.SourceName = string(v)
|
||||
case models.EventSourceUsername:
|
||||
e.SourceUsername = string(v)
|
||||
}
|
||||
|
||||
e.Pub()
|
||||
}
|
||||
223
jiacrontab_admin/daemon.go
Normal file
223
jiacrontab_admin/daemon.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/rpc"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetDaemonJobList(ctx *myctx) {
|
||||
var (
|
||||
reqBody GetJobListReqParams
|
||||
jobRet proto.QueryDaemonJobRet
|
||||
err error
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "DaemonJob.List", &proto.QueryJobArgs{
|
||||
Page: reqBody.Page,
|
||||
Pagesize: reqBody.Pagesize,
|
||||
SearchTxt: reqBody.SearchTxt,
|
||||
Root: ctx.claims.Root,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
UserID: ctx.claims.UserID,
|
||||
}, &jobRet); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": jobRet.List,
|
||||
"page": jobRet.Page,
|
||||
"pagesize": jobRet.Pagesize,
|
||||
"total": jobRet.Total,
|
||||
})
|
||||
}
|
||||
|
||||
func ActionDaemonTask(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reply []models.DaemonJob
|
||||
ok bool
|
||||
reqBody ActionTaskReqParams
|
||||
|
||||
methods = map[string]string{
|
||||
"start": "DaemonJob.Start",
|
||||
"delete": "DaemonJob.Delete",
|
||||
"stop": "DaemonJob.Stop",
|
||||
}
|
||||
|
||||
eDesc = map[string]string{
|
||||
"start": event_StartDaemonJob,
|
||||
"delete": event_DelDaemonJob,
|
||||
"stop": event_StopDaemonJob,
|
||||
}
|
||||
method string
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
}
|
||||
|
||||
if method, ok = methods[reqBody.Action]; !ok {
|
||||
ctx.respBasicError(errors.New("参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, method, proto.ActionJobsArgs{
|
||||
UserID: ctx.claims.UserID,
|
||||
JobIDs: reqBody.JobIDs,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
Root: ctx.claims.Root,
|
||||
}, &reply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(reply) > 0 {
|
||||
var targetNames []string
|
||||
for _, v := range reply {
|
||||
targetNames = append(targetNames, v.Name)
|
||||
}
|
||||
ctx.pubEvent(strings.Join(targetNames, ","), eDesc[reqBody.Action], models.EventSourceName(reqBody.Addr), reqBody)
|
||||
}
|
||||
|
||||
ctx.respSucc("", nil)
|
||||
}
|
||||
|
||||
// EditDaemonJob 修改常驻任务,jobID为0时新增
|
||||
func EditDaemonJob(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reply models.DaemonJob
|
||||
reqBody EditDaemonJobReqParams
|
||||
daemonJob models.DaemonJob
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
}
|
||||
|
||||
if !ctx.verifyNodePermission(reqBody.Addr) {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
daemonJob = models.DaemonJob{
|
||||
Name: reqBody.Name,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
ErrorMailNotify: reqBody.ErrorMailNotify,
|
||||
ErrorAPINotify: reqBody.ErrorAPINotify,
|
||||
ErrorDingdingNotify: reqBody.ErrorDingdingNotify,
|
||||
MailTo: reqBody.MailTo,
|
||||
APITo: reqBody.APITo,
|
||||
DingdingTo: reqBody.DingdingTo,
|
||||
UpdatedUserID: ctx.claims.UserID,
|
||||
UpdatedUsername: ctx.claims.Username,
|
||||
Command: reqBody.Command,
|
||||
WorkDir: reqBody.WorkDir,
|
||||
WorkEnv: reqBody.WorkEnv,
|
||||
WorkUser: reqBody.WorkUser,
|
||||
WorkIp: reqBody.WorkIp,
|
||||
Code: reqBody.Code,
|
||||
RetryNum: reqBody.RetryNum,
|
||||
FailRestart: reqBody.FailRestart,
|
||||
Status: models.StatusJobUnaudited,
|
||||
CreatedUserID: ctx.claims.UserID,
|
||||
CreatedUsername: ctx.claims.Username,
|
||||
}
|
||||
daemonJob.ID = reqBody.JobID
|
||||
if ctx.claims.Root || ctx.claims.GroupID == models.SuperGroup.ID {
|
||||
daemonJob.Status = models.StatusJobOk
|
||||
} else {
|
||||
daemonJob.Status = models.StatusJobUnaudited
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "DaemonJob.Edit", proto.EditDaemonJobArgs{
|
||||
GroupID: ctx.claims.GroupID,
|
||||
UserID: ctx.claims.UserID,
|
||||
Job: daemonJob,
|
||||
Root: ctx.claims.Root,
|
||||
}, &reply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(reply.Name, event_EditDaemonJob, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
ctx.respSucc("", reply)
|
||||
}
|
||||
|
||||
func GetDaemonJob(ctx *myctx) {
|
||||
var (
|
||||
reqBody GetJobReqParams
|
||||
daemonJob models.DaemonJob
|
||||
err error
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.verifyNodePermission(reqBody.Addr) {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "DaemonJob.Get", proto.GetJobArgs{
|
||||
UserID: ctx.claims.UserID,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
Root: ctx.claims.Root,
|
||||
JobID: reqBody.JobID,
|
||||
}, &daemonJob); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", daemonJob)
|
||||
}
|
||||
|
||||
func GetRecentDaemonLog(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
searchRet proto.SearchLogResult
|
||||
reqBody GetLogReqParams
|
||||
logList []string
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rpc.Call(reqBody.Addr, "DaemonJob.Log", proto.SearchLog{
|
||||
JobID: reqBody.JobID,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
Root: ctx.claims.Root,
|
||||
UserID: ctx.claims.UserID,
|
||||
Offset: reqBody.Offset,
|
||||
Pagesize: reqBody.Pagesize,
|
||||
Date: reqBody.Date,
|
||||
Pattern: reqBody.Pattern,
|
||||
IsTail: reqBody.IsTail,
|
||||
}, &searchRet); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logList = strings.Split(string(searchRet.Content), "\n")
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"logList": logList,
|
||||
"curAddr": reqBody.Addr,
|
||||
"offset": searchRet.Offset,
|
||||
"filesize": searchRet.FileSize,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
21
jiacrontab_admin/debug.go
Normal file
21
jiacrontab_admin/debug.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/base"
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
func stat(ctx *myctx) {
|
||||
data := base.Stat.Collect()
|
||||
ctx.JSON(data)
|
||||
}
|
||||
|
||||
func pprofHandler(ctx *myctx) {
|
||||
if h := pprof.Handler(ctx.Params().Get("key")); h != nil {
|
||||
h.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
|
||||
}
|
||||
}
|
||||
|
||||
func indexDebug(ctx *myctx) {
|
||||
pprof.Index(ctx.ResponseWriter(), ctx.Request())
|
||||
}
|
||||
66
jiacrontab_admin/group.go
Normal file
66
jiacrontab_admin/group.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
)
|
||||
|
||||
// GetGroupList 获得所有的分组列表
|
||||
func GetGroupList(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
groupList []models.Group
|
||||
count int64
|
||||
model = models.DB().Model(&models.Group{})
|
||||
reqBody GetGroupListReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
model.Where("name like ?", "%"+reqBody.SearchTxt+"%").Count(&count)
|
||||
|
||||
err = model.Where("name like ?", "%"+reqBody.SearchTxt+"%").Offset((reqBody.Page - 1) * reqBody.Pagesize).Order("updated_at desc").Limit(reqBody.Pagesize).Find(&groupList).Error
|
||||
if err != nil {
|
||||
ctx.respError(proto.Code_Error, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": groupList,
|
||||
"total": count,
|
||||
"page": reqBody.Page,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
|
||||
// EditGroup 编辑分组
|
||||
func EditGroup(ctx *myctx) {
|
||||
var (
|
||||
reqBody EditGroupReqParams
|
||||
err error
|
||||
group models.Group
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
group.ID = reqBody.GroupID
|
||||
group.Name = reqBody.GroupName
|
||||
|
||||
if err = group.Save(); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
ctx.pubEvent(group.Name, event_EditGroup, "", reqBody)
|
||||
ctx.respSucc("", nil)
|
||||
}
|
||||
124
jiacrontab_admin/ldap.go
Normal file
124
jiacrontab_admin/ldap.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
ld "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Ldap struct {
|
||||
BindUserDn string
|
||||
BindPwd string
|
||||
BaseOn string
|
||||
UserField string
|
||||
Addr string
|
||||
Timeout time.Duration
|
||||
fields []map[string]string
|
||||
queryFields []string
|
||||
lastSynced time.Time
|
||||
DisabledAnonymousQuery bool
|
||||
}
|
||||
|
||||
func (l *Ldap) connect() (*ld.Conn, error) {
|
||||
var err error
|
||||
conn, err := ld.DialURL(l.Addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn.SetTimeout(l.Timeout)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (l *Ldap) Login(username string, password string) (*models.User, error) {
|
||||
err := l.loadLdapFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := l.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if l.DisabledAnonymousQuery {
|
||||
err := conn.Bind(l.BindUserDn, l.BindPwd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
query := ld.NewSearchRequest(
|
||||
l.BaseOn,
|
||||
ld.ScopeWholeSubtree,
|
||||
ld.DerefAlways,
|
||||
0, 0, false,
|
||||
fmt.Sprintf("(%v=%v)", l.UserField, username),
|
||||
l.queryFields, nil,
|
||||
)
|
||||
ret, err := conn.Search(query)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("在Ldap搜索用户失败 - %v", err)
|
||||
}
|
||||
if len(ret.Entries) == 0 {
|
||||
return nil, fmt.Errorf("在Ldap中未查询到对应的用户信息 - %v", err)
|
||||
}
|
||||
|
||||
err = conn.Bind(ret.Entries[0].DN, password)
|
||||
if err != nil {
|
||||
return nil, errors.New("帐号或密码不正确")
|
||||
}
|
||||
return l.convert(ret.Entries[0])
|
||||
}
|
||||
|
||||
func (l *Ldap) loadLdapFields() error {
|
||||
var setting models.SysSetting
|
||||
var list []map[string]string
|
||||
|
||||
if time.Since(l.lastSynced).Hours() < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := models.DB().Model(&models.SysSetting{}).Where("class=?", 1).Find(&setting).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// err = json.Unmarshal(setting.Content, &list)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
for _, v := range list {
|
||||
if v["ldap_field_name"] != "" {
|
||||
l.fields = append(l.fields, v)
|
||||
}
|
||||
}
|
||||
|
||||
l.queryFields = []string{"dn"}
|
||||
for _, v := range l.fields {
|
||||
l.queryFields = append(l.queryFields, v["ldap_field_name"])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Ldap) convert(ldapUserInfo *ld.Entry) (*models.User, error) {
|
||||
var userinfo models.User
|
||||
for _, v := range l.fields {
|
||||
switch v["local_field_name"] {
|
||||
case "username":
|
||||
userinfo.Username = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
|
||||
case "gender":
|
||||
userinfo.Gender = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
|
||||
case "avatar":
|
||||
userinfo.Avatar = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
|
||||
case "email":
|
||||
userinfo.Mail = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
|
||||
}
|
||||
}
|
||||
err := models.DB().Where("username=?", userinfo.Username).FirstOrCreate(&userinfo).Error
|
||||
return &userinfo, err
|
||||
}
|
||||
168
jiacrontab_admin/node.go
Normal file
168
jiacrontab_admin/node.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetNodeList 获得任务节点列表
|
||||
// 支持获得所属分组节点,指定分组节点(超级管理员)
|
||||
func GetNodeList(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
nodeList []models.Node
|
||||
reqBody GetNodeListReqParams
|
||||
count int64
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.QueryGroupID != ctx.claims.GroupID && !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
cfg := ctx.adm.getOpts()
|
||||
maxClientAliveInterval := -1 * cfg.App.MaxClientAliveInterval
|
||||
|
||||
currentTime := time.Now().Add(time.Second * time.Duration(maxClientAliveInterval)).Format("2006-01-02 15:04:05")
|
||||
|
||||
//失联列表更新为已断开状态
|
||||
models.DB().Unscoped().Model(&models.Node{}).Where("updated_at<?", currentTime).Updates(map[string]interface{}{
|
||||
"disabled": true,
|
||||
})
|
||||
|
||||
model := models.DB()
|
||||
if reqBody.SearchTxt != "" {
|
||||
txt := "%" + reqBody.SearchTxt + "%"
|
||||
model = model.Where("(name like ? or addr like ?)", txt, txt)
|
||||
}
|
||||
|
||||
switch reqBody.QueryStatus {
|
||||
case 1:
|
||||
err = model.Preload("Group").Where("group_id=? and disabled=?",
|
||||
reqBody.QueryGroupID, false).Offset((reqBody.Page - 1) * reqBody.Pagesize).
|
||||
Order("id desc").Limit(reqBody.Pagesize).Find(&nodeList).Error
|
||||
|
||||
model.Model(&models.Node{}).Where("group_id=? and disabled=?",
|
||||
reqBody.QueryGroupID, false).Count(&count)
|
||||
case 2:
|
||||
err = model.Preload("Group").Where("group_id=? and disabled=?",
|
||||
reqBody.QueryGroupID, true).Offset((reqBody.Page - 1) * reqBody.Pagesize).Order("id desc").Limit(reqBody.Pagesize).Find(&nodeList).Error
|
||||
|
||||
model.Model(&models.Node{}).Where("group_id=? and disabled=?", reqBody.QueryGroupID, true).Count(&count)
|
||||
default:
|
||||
err = model.Preload("Group").Where("group_id=?",
|
||||
reqBody.QueryGroupID).Offset((reqBody.Page - 1) * reqBody.Pagesize).Order("id desc").Limit(reqBody.Pagesize).Find(&nodeList).Error
|
||||
|
||||
model.Model(&models.Node{}).Where("group_id=?",
|
||||
reqBody.QueryGroupID).Count(&count)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": nodeList,
|
||||
"total": count,
|
||||
"page": reqBody.Page,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteNode 删除分组内节点
|
||||
// 仅超级管理员有权限
|
||||
func DeleteNode(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody DeleteNodeReqParams
|
||||
node models.Node
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
}
|
||||
|
||||
// 普通用户不允许删除节点
|
||||
if !(ctx.isSuper() || (ctx.isRoot() && ctx.claims.GroupID == reqBody.GroupID)) {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if err = node.Delete(reqBody.GroupID, reqBody.Addr); err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(node.Addr, event_DelNodeDesc, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
ctx.respSucc("", nil)
|
||||
}
|
||||
|
||||
// GroupNode 超级管理员为node分组
|
||||
// 分组不存在时自动创建分组
|
||||
// copy超级管理员分组中的节点到新的分组
|
||||
func GroupNode(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody GroupNodeReqParams
|
||||
node models.Node
|
||||
)
|
||||
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = node.GroupNode(reqBody.Addr, reqBody.TargetGroupID,
|
||||
reqBody.TargetNodeName, reqBody.TargetGroupName); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(node.Group.Name, event_GroupNode, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
ctx.respSucc("", nil)
|
||||
}
|
||||
|
||||
// DeleteNode 删除分组内节点
|
||||
// 仅超级管理员有权限
|
||||
func CleanNodeLog(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody CleanNodeLogReqParams
|
||||
cleanRet proto.CleanNodeLogRet
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 普通用户不允许删除节点
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "Srv.CleanLogFiles", proto.CleanNodeLog{
|
||||
Unit: reqBody.Unit,
|
||||
Offset: reqBody.Offset,
|
||||
}, &cleanRet); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(fmt.Sprintf("%d %s", reqBody.Offset, reqBody.Unit), event_CleanNodeLog, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
ctx.respSucc("", cleanRet)
|
||||
}
|
||||
456
jiacrontab_admin/params.go
Normal file
456
jiacrontab_admin/params.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
paramsError = errors.New("参数错误")
|
||||
)
|
||||
|
||||
type Parameter interface {
|
||||
Verify(*myctx) error
|
||||
}
|
||||
|
||||
type JobReqParams struct {
|
||||
JobID uint `json:"jobID" rule:"required,请填写jobID"`
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
}
|
||||
|
||||
func (p *JobReqParams) Verify(*myctx) error {
|
||||
if p.JobID == 0 || p.Addr == "" {
|
||||
return paramsError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type JobsReqParams struct {
|
||||
JobIDs []uint `json:"jobIDs" `
|
||||
Addr string `json:"addr"`
|
||||
}
|
||||
|
||||
func (p *JobsReqParams) Verify(ctx *myctx) error {
|
||||
|
||||
if len(p.JobIDs) == 0 || p.Addr == "" {
|
||||
return paramsError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type EditJobReqParams struct {
|
||||
JobID uint `json:"jobID"`
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
IsSync bool `json:"isSync"`
|
||||
Name string `json:"name" rule:"required,请填写name"`
|
||||
Command []string `json:"command" rule:"required,请填写name"`
|
||||
Code string `json:"code"`
|
||||
Timeout int `json:"timeout"`
|
||||
MaxConcurrent uint `json:"maxConcurrent"`
|
||||
ErrorMailNotify bool `json:"errorMailNotify"`
|
||||
ErrorAPINotify bool `json:"errorAPINotify"`
|
||||
ErrorDingdingNotify bool `json:"errorDingdingNotify"`
|
||||
MailTo []string `json:"mailTo"`
|
||||
APITo []string `json:"APITo"`
|
||||
DingdingTo []string `json:"DingdingTo"`
|
||||
RetryNum int `json:"retryNum"`
|
||||
WorkDir string `json:"workDir"`
|
||||
WorkUser string `json:"workUser"`
|
||||
WorkEnv []string `json:"workEnv"`
|
||||
WorkIp []string `json:"workIp"`
|
||||
KillChildProcess bool `json:"killChildProcess"`
|
||||
DependJobs models.DependJobs `json:"dependJobs"`
|
||||
Month string `json:"month"`
|
||||
Weekday string `json:"weekday"`
|
||||
Day string `json:"day"`
|
||||
Hour string `json:"hour"`
|
||||
Minute string `json:"minute"`
|
||||
Second string `json:"second"`
|
||||
TimeoutTrigger []string `json:"timeoutTrigger"`
|
||||
}
|
||||
|
||||
func (p *EditJobReqParams) Verify(ctx *myctx) error {
|
||||
ts := map[string]bool{
|
||||
proto.TimeoutTrigger_CallApi: true,
|
||||
proto.TimeoutTrigger_SendEmail: true,
|
||||
proto.TimeoutTrigger_Kill: true,
|
||||
proto.TimeoutTrigger_DingdingWebhook: true,
|
||||
}
|
||||
|
||||
for _, v := range p.TimeoutTrigger {
|
||||
if !ts[v] {
|
||||
return fmt.Errorf("%s:%v", v, paramsError)
|
||||
}
|
||||
}
|
||||
|
||||
p.Command = util.FilterEmptyEle(p.Command)
|
||||
p.MailTo = util.FilterEmptyEle(p.MailTo)
|
||||
p.APITo = util.FilterEmptyEle(p.APITo)
|
||||
p.DingdingTo = util.FilterEmptyEle(p.DingdingTo)
|
||||
p.WorkEnv = util.FilterEmptyEle(p.WorkEnv)
|
||||
p.WorkIp = util.FilterEmptyEle(p.WorkIp)
|
||||
|
||||
if p.Month == "" {
|
||||
p.Month = "*"
|
||||
}
|
||||
|
||||
if p.Weekday == "" {
|
||||
p.Weekday = "*"
|
||||
}
|
||||
|
||||
if p.Day == "" {
|
||||
p.Day = "*"
|
||||
}
|
||||
|
||||
if p.Hour == "" {
|
||||
p.Hour = "*"
|
||||
}
|
||||
|
||||
if p.Minute == "" {
|
||||
p.Minute = "*"
|
||||
}
|
||||
|
||||
if p.Second == "" {
|
||||
p.Second = "*"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetLogReqParams struct {
|
||||
Addr string `json:"addr"`
|
||||
JobID uint `json:"jobID"`
|
||||
Date string `json:"date"`
|
||||
Pattern string `json:"pattern"`
|
||||
IsTail bool `json:"isTail"`
|
||||
Offset int64 `json:"offset"`
|
||||
Pagesize int `json:"pagesize"`
|
||||
}
|
||||
|
||||
func (p *GetLogReqParams) Verify(ctx *myctx) error {
|
||||
|
||||
if p.Pagesize <= 0 {
|
||||
p.Pagesize = 50
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteNodeReqParams struct {
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
GroupID uint `json:"groupID"`
|
||||
}
|
||||
|
||||
func (p *DeleteNodeReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type CleanNodeLogReqParams struct {
|
||||
Unit string `json:"unit" rule:"required,请填写时间单位"`
|
||||
Offset int `json:"offset"`
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
}
|
||||
|
||||
func (p *CleanNodeLogReqParams) Verify(ctx *myctx) error {
|
||||
if p.Unit != "day" && p.Unit != "month" {
|
||||
return errors.New("不支持的时间单位")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendTestMailReqParams struct {
|
||||
MailTo string `json:"mailTo" rule:"required,请填写mailTo"`
|
||||
}
|
||||
|
||||
func (p *SendTestMailReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type SystemInfoReqParams struct {
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
}
|
||||
|
||||
func (p *SystemInfoReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetJobListReqParams struct {
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
SearchTxt string `json:"searchTxt"`
|
||||
PageReqParams
|
||||
}
|
||||
|
||||
func (p *GetJobListReqParams) Verify(ctx *myctx) error {
|
||||
|
||||
if p.Page <= 1 {
|
||||
p.Page = 1
|
||||
}
|
||||
|
||||
if p.Pagesize <= 0 {
|
||||
p.Pagesize = 50
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetGroupListReqParams struct {
|
||||
SearchTxt string `json:"searchTxt"`
|
||||
PageReqParams
|
||||
}
|
||||
|
||||
func (p *GetGroupListReqParams) Verify(ctx *myctx) error {
|
||||
|
||||
if p.Page <= 1 {
|
||||
p.Page = 1
|
||||
}
|
||||
|
||||
if p.Pagesize <= 0 {
|
||||
p.Pagesize = 50
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ActionTaskReqParams struct {
|
||||
Action string `json:"action" rule:"required,请填写action"`
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
JobIDs []uint `json:"jobIDs" rule:"required,请填写jobIDs"`
|
||||
}
|
||||
|
||||
func (p *ActionTaskReqParams) Verify(ctx *myctx) error {
|
||||
if len(p.JobIDs) == 0 {
|
||||
return paramsError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EditDaemonJobReqParams struct {
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
JobID uint `json:"jobID"`
|
||||
Name string `json:"name" rule:"required,请填写name"`
|
||||
MailTo []string `json:"mailTo"`
|
||||
APITo []string `json:"APITo"`
|
||||
DingdingTo []string `json:"DingdingTo"`
|
||||
Command []string `json:"command" rule:"required,请填写command"`
|
||||
Code string `json:"code"`
|
||||
WorkUser string `json:"workUser"`
|
||||
WorkIp []string `json:"workIp"`
|
||||
WorkEnv []string `json:"workEnv"`
|
||||
WorkDir string `json:"workDir"`
|
||||
FailRestart bool `json:"failRestart"`
|
||||
RetryNum int `json:"retryNum"`
|
||||
ErrorMailNotify bool `json:"errorMailNotify"`
|
||||
ErrorAPINotify bool `json:"errorAPINotify"`
|
||||
ErrorDingdingNotify bool `json:"errorDingdingNotify"`
|
||||
}
|
||||
|
||||
func (p *EditDaemonJobReqParams) Verify(ctx *myctx) error {
|
||||
p.MailTo = util.FilterEmptyEle(p.MailTo)
|
||||
p.APITo = util.FilterEmptyEle(p.APITo)
|
||||
p.Command = util.FilterEmptyEle(p.Command)
|
||||
p.WorkEnv = util.FilterEmptyEle(p.WorkEnv)
|
||||
p.WorkIp = util.FilterEmptyEle(p.WorkIp)
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetJobReqParams struct {
|
||||
JobID uint `json:"jobID" rule:"required,请填写jobID"`
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
}
|
||||
|
||||
func (p *GetJobReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserReqParams struct {
|
||||
Username string `json:"username" rule:"required,请输入用户名"`
|
||||
Passwd string `json:"passwd,omitempty" rule:"required,请输入密码"`
|
||||
GroupID uint `json:"groupID"`
|
||||
GroupName string `json:"groupName"`
|
||||
Avatar string `json:"avatar"`
|
||||
Root bool `json:"root"`
|
||||
Mail string `json:"mail"`
|
||||
}
|
||||
|
||||
func (p *UserReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type InitAppReqParams struct {
|
||||
Username string `json:"username" rule:"required,请输入用户名"`
|
||||
Passwd string `json:"passwd" rule:"required,请输入密码"`
|
||||
Avatar string `json:"avatar"`
|
||||
Mail string `json:"mail"`
|
||||
}
|
||||
|
||||
func (p *InitAppReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type EditUserReqParams struct {
|
||||
UserID uint `json:"userID" rule:"required,缺少userID"`
|
||||
Username string `json:"username"`
|
||||
Passwd string `json:"passwd"`
|
||||
OldPwd string `json:"oldpwd"`
|
||||
Avatar string `json:"avatar"`
|
||||
Mail string `json:"mail"`
|
||||
}
|
||||
|
||||
func (p *EditUserReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteUserReqParams struct {
|
||||
UserID uint `json:"userID" rule:"required,缺少userID"`
|
||||
}
|
||||
|
||||
func (p *DeleteUserReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type LoginReqParams struct {
|
||||
Username string `json:"username" rule:"required,请输入用户名"`
|
||||
Passwd string `json:"passwd" rule:"required,请输入密码"`
|
||||
Remember bool `json:"remember"`
|
||||
IsLdap bool `json:"is_ldap"`
|
||||
}
|
||||
|
||||
func (p *LoginReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type PageReqParams struct {
|
||||
Page int `json:"page"`
|
||||
Pagesize int `json:"pagesize"`
|
||||
}
|
||||
|
||||
type GetNodeListReqParams struct {
|
||||
PageReqParams
|
||||
SearchTxt string `json:"searchTxt"`
|
||||
QueryGroupID uint `json:"queryGroupID"`
|
||||
QueryStatus uint `json:"queryStatus"`
|
||||
}
|
||||
|
||||
func (p *GetNodeListReqParams) Verify(ctx *myctx) error {
|
||||
|
||||
if p.Page == 0 {
|
||||
p.Page = 1
|
||||
}
|
||||
if p.Pagesize <= 0 {
|
||||
p.Pagesize = 50
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EditGroupReqParams struct {
|
||||
GroupID uint `json:"groupID" rule:"required,请填写groupID"`
|
||||
GroupName string `json:"groupName" rule:"required,请填写groupName"`
|
||||
}
|
||||
|
||||
func (p *EditGroupReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetGroupReqParams struct {
|
||||
TargetGroupID uint `json:"targetGroupID"`
|
||||
TargetGroupName string `json:"targetGroupName"`
|
||||
UserID uint `json:"userID" rule:"required,请填写用户ID"`
|
||||
Root bool `json:"root"`
|
||||
}
|
||||
|
||||
func (p *SetGroupReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReadMoreReqParams struct {
|
||||
LastID int `json:"lastID"`
|
||||
Pagesize int `json:"pagesize"`
|
||||
Keywords string `json:"keywords"`
|
||||
Orderby string `json:"orderby"`
|
||||
}
|
||||
|
||||
func (p *ReadMoreReqParams) Verify(ctx *myctx) error {
|
||||
if p.Pagesize == 0 {
|
||||
p.Pagesize = 50
|
||||
}
|
||||
|
||||
if p.Orderby == "" {
|
||||
p.Orderby = "desc"
|
||||
}
|
||||
|
||||
p.Keywords = strings.TrimSpace(p.Keywords)
|
||||
return nil
|
||||
}
|
||||
|
||||
type GroupNodeReqParams struct {
|
||||
Addr string `json:"addr" rule:"required,请填写addr"`
|
||||
TargetNodeName string `json:"targetNodeName"`
|
||||
TargetGroupName string `json:"targetGroupName"`
|
||||
TargetGroupID uint `json:"targetGroupID"`
|
||||
}
|
||||
|
||||
func (p *GroupNodeReqParams) Verify(ctx *myctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuditJobReqParams struct {
|
||||
JobsReqParams
|
||||
JobType string `json:"jobType"`
|
||||
}
|
||||
|
||||
func (p *AuditJobReqParams) Verify(ctx *myctx) error {
|
||||
|
||||
if p.Addr == "" {
|
||||
return paramsError
|
||||
}
|
||||
|
||||
jobTypeMap := map[string]bool{
|
||||
"crontab": true,
|
||||
"daemon": true,
|
||||
}
|
||||
|
||||
if err := p.JobsReqParams.Verify(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !jobTypeMap[p.JobType] {
|
||||
return paramsError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetUsersParams struct {
|
||||
PageReqParams
|
||||
SearchTxt string `json:"searchTxt"`
|
||||
IsAll bool `json:"isAll"`
|
||||
QueryGroupID uint `json:"queryGroupID"`
|
||||
}
|
||||
|
||||
func (p *GetUsersParams) Verify(ctx *myctx) error {
|
||||
|
||||
if p.Page <= 1 {
|
||||
p.Page = 1
|
||||
}
|
||||
|
||||
if p.Pagesize <= 0 {
|
||||
p.Pagesize = 50
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CleanLogParams struct {
|
||||
IsEvent bool `json:"isEvent"`
|
||||
Unit string `json:"unit" rule:"required,请填写时间单位"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
func (c *CleanLogParams) Verify(ctx *myctx) error {
|
||||
if c.Unit != "day" && c.Unit != "month" {
|
||||
return errors.New("不支持的时间单位")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
62
jiacrontab_admin/recover.go
Normal file
62
jiacrontab_admin/recover.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jiacrontab/pkg/base"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func getRequestLogs(ctx *myctx) string {
|
||||
var status, ip, method, path string
|
||||
status = strconv.Itoa(ctx.GetStatusCode())
|
||||
path = ctx.Path()
|
||||
method = ctx.Method()
|
||||
ip = ctx.RemoteAddr()
|
||||
// the date should be logged by iris' Logger, so we skip them
|
||||
return fmt.Sprintf("%v %s %s %s", status, path, method, ip)
|
||||
}
|
||||
|
||||
func newRecover(adm *Admin) iris.Handler {
|
||||
return func(c iris.Context) {
|
||||
ctx := wrapCtx(c, adm)
|
||||
base.Stat.AddConcurrentCount()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
|
||||
base.Stat.AddErrorCount(ctx.RequestPath(true), fmt.Errorf("%v", err), 1)
|
||||
|
||||
if ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
var stacktrace string
|
||||
for i := 1; ; i++ {
|
||||
_, f, l, got := runtime.Caller(i)
|
||||
if !got {
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
stacktrace += fmt.Sprintf("%s:%d\n", f, l)
|
||||
}
|
||||
|
||||
// when stack finishes
|
||||
logMessage := fmt.Sprintf("Recovered from a route's Handler('%s')\n", ctx.HandlerName())
|
||||
logMessage += fmt.Sprintf("At Request: %s\n", getRequestLogs(ctx))
|
||||
logMessage += fmt.Sprintf("Trace: %s\n", err)
|
||||
logMessage += fmt.Sprintf("\n%s", stacktrace)
|
||||
ctx.Application().Logger().Warn(logMessage)
|
||||
|
||||
ctx.StatusCode(500)
|
||||
ctx.respError(http.StatusInternalServerError, fmt.Sprint(err), nil)
|
||||
ctx.StopExecution()
|
||||
}
|
||||
}()
|
||||
base.Stat.AddRequestCount(ctx.RequestPath(true), ctx.GetStatusCode(), 1)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
26
jiacrontab_admin/runtime.go
Normal file
26
jiacrontab_admin/runtime.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/version"
|
||||
)
|
||||
|
||||
func SystemInfo(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
info map[string]interface{}
|
||||
reqBody SystemInfoReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rpcCall(reqBody.Addr, "Srv.SystemInfo", proto.EmptyArgs{}, &info); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
info["version"] = version.String("jiacrontab")
|
||||
ctx.respSucc("", info)
|
||||
}
|
||||
143
jiacrontab_admin/srv.go
Normal file
143
jiacrontab_admin/srv.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/mailer"
|
||||
"jiacrontab/pkg/proto"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
type Srv struct {
|
||||
adm *Admin
|
||||
}
|
||||
|
||||
func NewSrv(adm *Admin) *Srv {
|
||||
return &Srv{
|
||||
adm: adm,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Srv) Register(args map[uint]models.Node, reply *bool) error {
|
||||
*reply = true
|
||||
|
||||
for _, node := range args {
|
||||
ret := models.DB().Unscoped().Model(&models.Node{}).Where("addr=? and group_id=?", node.Addr, node.GroupID).Updates(map[string]interface{}{
|
||||
"daemon_task_num": node.DaemonTaskNum,
|
||||
"crontab_task_num": node.CrontabTaskNum,
|
||||
"crontab_job_audit_num": node.CrontabJobAuditNum,
|
||||
"daemon_job_audit_num": node.DaemonJobAuditNum,
|
||||
"crontab_job_fail_num": node.CrontabJobFailNum,
|
||||
})
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
if node.GroupID == models.SuperGroup.ID {
|
||||
ret = models.DB().Unscoped().Model(&models.Node{}).Where("addr=?", node.Addr).Updates(map[string]interface{}{
|
||||
"name": node.Name,
|
||||
"deleted_at": nil,
|
||||
"disabled": false,
|
||||
})
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
}
|
||||
|
||||
if ret.RowsAffected == 0 && node.GroupID == models.SuperGroup.ID {
|
||||
ret = models.DB().Create(&node)
|
||||
}
|
||||
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Srv) ExecDepend(args proto.DepJobs, reply *bool) error {
|
||||
log.Infof("Callee Srv.ExecDepend jobID:%d", args[0].JobID)
|
||||
*reply = true
|
||||
for _, v := range args {
|
||||
if err := rpcCall(v.Dest, "CrontabJob.ExecDepend", v, &reply); err != nil {
|
||||
*reply = false
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Srv) SetDependDone(args proto.DepJob, reply *bool) error {
|
||||
log.Infof("Callee Srv.SetDependDone jobID:%d", args.JobID)
|
||||
*reply = true
|
||||
if err := rpcCall(args.Dest, "CrontabJob.SetDependDone", args, &reply); err != nil {
|
||||
*reply = false
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Srv) SendMail(args proto.SendMail, reply *bool) error {
|
||||
var (
|
||||
err error
|
||||
cfg = s.adm.getOpts()
|
||||
)
|
||||
if cfg.Mailer.Enabled {
|
||||
err = mailer.SendMail(args.MailTo, args.Subject, args.Content)
|
||||
}
|
||||
*reply = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Srv) PushJobLog(args models.JobHistory, reply *bool) error {
|
||||
models.PushJobHistory(&args)
|
||||
*reply = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Srv) ApiPost(args proto.ApiPost, reply *bool) error {
|
||||
var (
|
||||
err error
|
||||
errs []error
|
||||
)
|
||||
|
||||
for _, url := range args.Urls {
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Minute,
|
||||
}
|
||||
|
||||
response, err := client.Post(url, "application/json", strings.NewReader(args.Data))
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
log.Errorf("post url %s fail: %s", url, err)
|
||||
continue
|
||||
}
|
||||
defer response.Body.Close()
|
||||
io.Copy(ioutil.Discard, response.Body)
|
||||
}
|
||||
|
||||
for _, v := range errs {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s\n%s", err, v)
|
||||
} else {
|
||||
err = v
|
||||
}
|
||||
}
|
||||
|
||||
*reply = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Srv) Ping(args *proto.EmptyArgs, reply *proto.EmptyReply) error {
|
||||
return nil
|
||||
}
|
||||
76
jiacrontab_admin/system.go
Normal file
76
jiacrontab_admin/system.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func LogInfo(ctx *myctx) {
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
var jobTotal int64
|
||||
err := models.DB().Model(&models.JobHistory{}).Count(&jobTotal).Error
|
||||
if err != nil {
|
||||
ctx.respDBError(err)
|
||||
}
|
||||
var eventTotal int64
|
||||
err = models.DB().Model(&models.Event{}).Count(&eventTotal).Error
|
||||
if err != nil {
|
||||
ctx.respDBError(err)
|
||||
}
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"event_total": eventTotal,
|
||||
"job_total": jobTotal,
|
||||
})
|
||||
}
|
||||
|
||||
func CleanLog(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody CleanLogParams
|
||||
isSuper = ctx.isSuper()
|
||||
)
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
if !isSuper {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
offset := time.Now()
|
||||
if reqBody.Unit == "day" {
|
||||
offset = offset.AddDate(0, 0, -reqBody.Offset)
|
||||
}
|
||||
if reqBody.Unit == "month" {
|
||||
offset = offset.AddDate(0, -reqBody.Offset, 0)
|
||||
}
|
||||
var tx *gorm.DB
|
||||
if reqBody.IsEvent {
|
||||
tx = models.DB().Where("created_at<?", offset).Unscoped().Delete(&models.Event{})
|
||||
|
||||
} else {
|
||||
tx = models.DB().Where("created_at<?", offset).Unscoped().Delete(&models.JobHistory{})
|
||||
}
|
||||
err = tx.Error
|
||||
if err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
|
||||
}
|
||||
if reqBody.IsEvent {
|
||||
ctx.pubEvent(fmt.Sprintf("%d %s", reqBody.Offset, reqBody.Unit), event_CleanUserEvent, "", reqBody)
|
||||
} else {
|
||||
ctx.pubEvent(fmt.Sprintf("%d %s", reqBody.Offset, reqBody.Unit), event_CleanJobHistory, "", reqBody)
|
||||
}
|
||||
|
||||
ctx.respSucc("清理成功", map[string]interface{}{
|
||||
"total": tx.RowsAffected,
|
||||
})
|
||||
return
|
||||
}
|
||||
501
jiacrontab_admin/user.go
Normal file
501
jiacrontab_admin/user.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/util"
|
||||
"jiacrontab/pkg/version"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
type CustomerClaims struct {
|
||||
jwt.StandardClaims
|
||||
Version int64
|
||||
UserID uint
|
||||
Mail string
|
||||
Username string
|
||||
GroupID uint
|
||||
Root bool
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func Login(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody LoginReqParams
|
||||
user models.User
|
||||
customerClaims CustomerClaims
|
||||
cfg = ctx.adm.getOpts()
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.IsLdap {
|
||||
luser, err := ctx.adm.ldap.Login(reqBody.Username, reqBody.Passwd)
|
||||
if err != nil {
|
||||
ctx.respAuthFailed(err)
|
||||
return
|
||||
}
|
||||
user = *luser
|
||||
} else {
|
||||
if !user.Verify(reqBody.Username, reqBody.Passwd) {
|
||||
ctx.respAuthFailed(errors.New("帐号或密码不正确"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
customerClaims.ExpiresAt = cfg.Jwt.Expires + time.Now().Unix()
|
||||
customerClaims.Username = reqBody.Username
|
||||
customerClaims.UserID = user.ID
|
||||
customerClaims.Mail = user.Mail
|
||||
customerClaims.GroupID = user.GroupID
|
||||
customerClaims.Root = user.Root
|
||||
customerClaims.Version = user.Version
|
||||
|
||||
if reqBody.Remember {
|
||||
customerClaims.ExpiresAt = time.Now().Add(24 * 30 * time.Hour).Unix()
|
||||
}
|
||||
|
||||
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, customerClaims).SignedString([]byte(cfg.Jwt.SigningKey))
|
||||
|
||||
if err != nil {
|
||||
ctx.respAuthFailed(errors.New("无法生成访问凭证"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"token": token,
|
||||
"groupID": user.GroupID,
|
||||
"root": user.Root,
|
||||
"mail": user.Mail,
|
||||
"username": user.Username,
|
||||
"userID": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func GetActivityList(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody ReadMoreReqParams
|
||||
events []models.Event
|
||||
isSuper bool
|
||||
model = models.DB()
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.isSuper() {
|
||||
isSuper = true
|
||||
}
|
||||
|
||||
if reqBody.LastID == 0 {
|
||||
if !isSuper {
|
||||
model = model.Where("group_id=?", ctx.claims.GroupID)
|
||||
}
|
||||
|
||||
} else {
|
||||
if !isSuper {
|
||||
model = model.Where("group_id=? and id<?", ctx.claims.GroupID, reqBody.LastID)
|
||||
} else {
|
||||
model = model.Where("id<?", reqBody.LastID)
|
||||
}
|
||||
}
|
||||
if reqBody.Keywords != "" {
|
||||
txt := "%" + reqBody.Keywords + "%"
|
||||
model = model.Where(`(
|
||||
username like ? or
|
||||
event_desc like ? or
|
||||
target_name like ? or
|
||||
source_username like ? or
|
||||
source_name like ? or
|
||||
content like ?)`, txt, txt, txt, txt, txt, txt)
|
||||
}
|
||||
err = model.Order(fmt.Sprintf("created_at %s", reqBody.Orderby)).
|
||||
Limit(reqBody.Pagesize).
|
||||
Find(&events).Error
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": events,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
|
||||
func GetJobHistory(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody ReadMoreReqParams
|
||||
historys []models.JobHistory
|
||||
addrs []string
|
||||
model = models.DB()
|
||||
isSuper = ctx.isSuper()
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if addrs, err = ctx.getGroupAddr(); err != nil {
|
||||
ctx.respError(proto.Code_Error, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.Keywords != "" {
|
||||
txt := "%" + reqBody.Keywords + "%"
|
||||
model = model.Where("(job_name like ? or addr like ? or exit_msg like ?)",
|
||||
txt, txt, txt)
|
||||
}
|
||||
|
||||
if reqBody.LastID == 0 {
|
||||
if !isSuper {
|
||||
model = model.Where("addr in (?)", addrs)
|
||||
}
|
||||
|
||||
err = model.Order(fmt.Sprintf("created_at %s", reqBody.Orderby)).
|
||||
Limit(reqBody.Pagesize).
|
||||
Find(&historys).Error
|
||||
} else {
|
||||
if !isSuper {
|
||||
model = model.Where("addr in (?) and id<?", addrs, reqBody.LastID)
|
||||
} else {
|
||||
model = model.Where("id<?", reqBody.LastID)
|
||||
}
|
||||
err = model.Where("addr in (?) and id<?", addrs, reqBody.LastID).
|
||||
Order(fmt.Sprintf("created_at %s", reqBody.Orderby)).
|
||||
Limit(reqBody.Pagesize).
|
||||
Find(&historys).Error
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": historys,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
|
||||
func AuditJob(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
reqBody AuditJobReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.verifyNodePermission(reqBody.Addr) {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.claims.GroupID != models.SuperGroup.ID && !ctx.claims.Root {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.JobType == "crontab" {
|
||||
var reply []models.CrontabJob
|
||||
if err = rpcCall(reqBody.Addr, "CrontabJob.Audit", proto.AuditJobArgs{
|
||||
Root: ctx.claims.Root,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
JobIDs: reqBody.JobIDs,
|
||||
}, &reply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
var targetNames []string
|
||||
for _, v := range reply {
|
||||
targetNames = append(targetNames, v.Name)
|
||||
}
|
||||
ctx.pubEvent(strings.Join(targetNames, ","), event_AuditCrontabJob, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
} else {
|
||||
var reply []models.DaemonJob
|
||||
if err = rpcCall(reqBody.Addr, "DaemonJob.Audit", proto.AuditJobArgs{
|
||||
Root: ctx.claims.Root,
|
||||
GroupID: ctx.claims.GroupID,
|
||||
JobIDs: reqBody.JobIDs,
|
||||
}, &reply); err != nil {
|
||||
ctx.respRPCError(err)
|
||||
return
|
||||
}
|
||||
var targetNames []string
|
||||
for _, v := range reply {
|
||||
targetNames = append(targetNames, v.Name)
|
||||
}
|
||||
ctx.pubEvent(strings.Join(targetNames, ","), event_AuditDaemonJob, models.EventSourceName(reqBody.Addr), reqBody)
|
||||
}
|
||||
|
||||
ctx.respSucc("", nil)
|
||||
}
|
||||
|
||||
// Signup 注册新用户
|
||||
func Signup(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
user models.User
|
||||
reqBody UserReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.GroupName != "" {
|
||||
group := models.Group{
|
||||
Name: reqBody.GroupName,
|
||||
}
|
||||
if err = models.DB().Save(&group).Error; err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
reqBody.GroupID = group.ID
|
||||
}
|
||||
|
||||
user.Username = reqBody.Username
|
||||
user.Passwd = reqBody.Passwd
|
||||
user.GroupID = reqBody.GroupID
|
||||
user.Root = reqBody.Root
|
||||
user.Avatar = reqBody.Avatar
|
||||
user.Mail = reqBody.Mail
|
||||
reqBody.Passwd = ""
|
||||
if user.GroupID == models.SuperGroup.ID {
|
||||
user.Root = true
|
||||
}
|
||||
|
||||
if err = user.Create(); err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(user.Username, event_SignUpUser, "", reqBody)
|
||||
ctx.respSucc("", true)
|
||||
}
|
||||
|
||||
func EditUser(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
user models.User
|
||||
reqBody EditUserReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// change password
|
||||
if reqBody.OldPwd != "" && reqBody.Passwd != "" {
|
||||
if !user.VerifyByUserId(reqBody.UserID, reqBody.OldPwd) {
|
||||
ctx.respParamError(errors.New("密码错误"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user.ID = reqBody.UserID
|
||||
user.Passwd = reqBody.Passwd
|
||||
user.Avatar = reqBody.Avatar
|
||||
user.Mail = reqBody.Mail
|
||||
|
||||
if err = user.Update(); err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(user.Username, event_EditUser, "", reqBody)
|
||||
ctx.respSucc("", true)
|
||||
}
|
||||
|
||||
func DeleteUser(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
user models.User
|
||||
reqBody DeleteUserReqParams
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
user.ID = reqBody.UserID
|
||||
if err = user.Delete(); err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
ctx.pubEvent(user.Username, event_DeleteUser, "", reqBody)
|
||||
ctx.respSucc("", true)
|
||||
}
|
||||
|
||||
// UserStat 统计信息
|
||||
func UserStat(ctx *myctx) {
|
||||
var (
|
||||
err error
|
||||
auditNumStat struct {
|
||||
CrontabJobAuditNum uint
|
||||
DaemonJobAuditNum uint
|
||||
CrontabJobFailNum uint
|
||||
DaemonTaskNum uint
|
||||
CrontabTaskNum uint
|
||||
NodeNum uint
|
||||
}
|
||||
cfg = ctx.adm.getOpts()
|
||||
)
|
||||
|
||||
err = models.DB().Raw(
|
||||
`select
|
||||
sum(crontab_job_audit_num) as crontab_job_audit_num,
|
||||
sum(daemon_job_audit_num) as daemon_job_audit_num,
|
||||
sum(crontab_job_fail_num) as crontab_job_fail_num,
|
||||
sum(daemon_task_num) as daemon_task_num,
|
||||
sum(crontab_task_num) as crontab_task_num,
|
||||
count(*) as node_num
|
||||
from nodes
|
||||
where group_id=? and deleted_at is null`, ctx.claims.GroupID).Scan(&auditNumStat).Error
|
||||
if err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"systemInfo": util.SystemInfo(cfg.ServerStartTime),
|
||||
"auditStat": auditNumStat,
|
||||
"version": version.String(cfg.App.AppName),
|
||||
})
|
||||
}
|
||||
|
||||
// GroupUser 超级管理员设置普通用户分组
|
||||
func GroupUser(ctx *myctx) {
|
||||
var (
|
||||
reqBody SetGroupReqParams
|
||||
err error
|
||||
user models.User
|
||||
group models.Group
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.isSuper() {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.TargetGroupName != "" {
|
||||
group.Name = reqBody.TargetGroupName
|
||||
if err = models.DB().Save(&group).Error; err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
reqBody.TargetGroupID = group.ID
|
||||
}
|
||||
|
||||
user.ID = reqBody.UserID
|
||||
user.GroupID = reqBody.TargetGroupID
|
||||
|
||||
if reqBody.TargetGroupID == models.SuperGroup.ID {
|
||||
user.Root = true
|
||||
} else {
|
||||
user.Root = reqBody.Root
|
||||
}
|
||||
|
||||
if err = user.SetGroup(&group); err != nil {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.pubEvent(group.Name, event_GroupUser, models.EventSourceUsername(user.Username), reqBody)
|
||||
ctx.respSucc("", nil)
|
||||
}
|
||||
|
||||
// GetUserList 获得用户列表
|
||||
// 支持获得全部用户,所属分组用户,指定分组用户(超级管理员)
|
||||
func GetUserList(ctx *myctx) {
|
||||
var (
|
||||
reqBody GetUsersParams
|
||||
userList []models.User
|
||||
err error
|
||||
total int64
|
||||
)
|
||||
|
||||
if err = ctx.Valid(&reqBody); err != nil {
|
||||
ctx.respParamError(err)
|
||||
}
|
||||
|
||||
if reqBody.IsAll && ctx.claims.GroupID != models.SuperGroup.ID {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if !reqBody.IsAll && reqBody.QueryGroupID != ctx.claims.GroupID && ctx.claims.GroupID != models.SuperGroup.ID {
|
||||
ctx.respNotAllowed()
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.QueryGroupID == 0 {
|
||||
reqBody.QueryGroupID = ctx.claims.GroupID
|
||||
}
|
||||
|
||||
m := models.DB().Model(&models.User{})
|
||||
if reqBody.IsAll {
|
||||
err = m.Where("username like ?", "%"+reqBody.SearchTxt+"%").Count(&total).Error
|
||||
} else {
|
||||
err = m.Where("group_id=? and username like ?", reqBody.QueryGroupID, "%"+reqBody.SearchTxt+"%").Count(&total).Error
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.respBasicError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.IsAll {
|
||||
err = models.DB().Preload("Group").Where("username like ?", "%"+reqBody.SearchTxt+"%").Order("id desc").Offset((reqBody.Page - 1) * reqBody.Pagesize).Limit(reqBody.Pagesize).Find(&userList).Error
|
||||
} else {
|
||||
err = models.DB().Preload("Group").Where("group_id=? and username like ?", reqBody.QueryGroupID, "%"+reqBody.SearchTxt+"%").Offset((reqBody.Page - 1) * reqBody.Pagesize).Limit(reqBody.Pagesize).Find(&userList).Error
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.respDBError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.respSucc("", map[string]interface{}{
|
||||
"list": userList,
|
||||
"total": total,
|
||||
"page": reqBody.Page,
|
||||
"pagesize": reqBody.Pagesize,
|
||||
})
|
||||
}
|
||||
75
jiacrontab_admin/util.go
Normal file
75
jiacrontab_admin/util.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/rpc"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
func rpcCall(addr string, serviceMethod string, args interface{}, reply interface{}) error {
|
||||
err := rpc.Call(addr, serviceMethod, args, reply)
|
||||
if err != nil {
|
||||
log.Errorf("rpcCall(%s->%s):%v", addr, serviceMethod, err)
|
||||
}
|
||||
if err == rpc.ErrRpc || err == rpc.ErrShutdown {
|
||||
models.DB().Unscoped().Model(&models.Node{}).Where("addr=?", addr).Update("disabled", true)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validStructRule(i interface{}) error {
|
||||
rt := reflect.TypeOf(i)
|
||||
rv := reflect.ValueOf(i)
|
||||
|
||||
if rt.Kind() == reflect.Ptr {
|
||||
rt = rt.Elem()
|
||||
}
|
||||
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
sf := rt.Field(i)
|
||||
r := sf.Tag.Get("rule")
|
||||
br := sf.Tag.Get("bind")
|
||||
|
||||
if br == "required" && rv.Field(i).Kind() == reflect.Ptr {
|
||||
if rv.Field(i).IsNil() {
|
||||
return errors.New(sf.Name + " is required")
|
||||
}
|
||||
}
|
||||
|
||||
if r == "" {
|
||||
continue
|
||||
}
|
||||
if rs := strings.Split(r, ","); len(rs) == 2 {
|
||||
rvf := rv.Field(i)
|
||||
if rs[0] == "required" {
|
||||
switch rvf.Kind() {
|
||||
case reflect.String:
|
||||
if rvf.Interface() == "" {
|
||||
return errors.New(rs[1])
|
||||
}
|
||||
case reflect.Array, reflect.Map:
|
||||
if rvf.Len() == 0 {
|
||||
return errors.New(rs[1])
|
||||
}
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if rvf.Interface() == 0 {
|
||||
return errors.New(rs[1])
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
331
jiacrontabd/cmd.go
Normal file
331
jiacrontabd/cmd.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"jiacrontab/pkg/kproc"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/util"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
type cmdUint struct {
|
||||
ctx context.Context
|
||||
id uint
|
||||
args [][]string
|
||||
logDir string
|
||||
content []byte
|
||||
logFile *os.File
|
||||
label string
|
||||
user string
|
||||
logPath string
|
||||
verboseLog bool
|
||||
exportLog bool
|
||||
ignoreFileLog bool
|
||||
env []string
|
||||
ip []string
|
||||
killChildProcess bool
|
||||
dir string
|
||||
startTime time.Time
|
||||
costTime time.Duration
|
||||
jd *Jiacrontabd
|
||||
}
|
||||
|
||||
func (cu *cmdUint) release() {
|
||||
if cu.logFile != nil {
|
||||
cu.logFile.Close()
|
||||
}
|
||||
cu.costTime = time.Now().Sub(cu.startTime)
|
||||
}
|
||||
|
||||
func (cu *cmdUint) launch() error {
|
||||
//todo: 需要添加 ip 校验
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Errorf("wrapExecScript error:%v\n%s", err, debug.Stack())
|
||||
}
|
||||
cu.release()
|
||||
}()
|
||||
cfg := cu.jd.getOpts()
|
||||
cu.startTime = time.Now()
|
||||
|
||||
var err error
|
||||
|
||||
if err = cu.setLogFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(cu.args) > 1 {
|
||||
err = cu.pipeExec()
|
||||
} else {
|
||||
err = cu.exec()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var errMsg string
|
||||
var prefix string
|
||||
if cu.verboseLog {
|
||||
prefix = fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
|
||||
errMsg = prefix + err.Error() + "\n"
|
||||
} else {
|
||||
prefix = fmt.Sprintf("[%s %s] ", cfg.BoardcastAddr, cu.label)
|
||||
errMsg = prefix + err.Error() + "\n"
|
||||
}
|
||||
|
||||
cu.writeLog([]byte(errMsg))
|
||||
if cu.exportLog {
|
||||
cu.content = append(cu.content, []byte(errMsg)...)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cu *cmdUint) setLogFile() error {
|
||||
var err error
|
||||
|
||||
if cu.ignoreFileLog {
|
||||
return nil
|
||||
}
|
||||
if cu.logPath == "" {
|
||||
cu.logPath = filepath.Join(cu.logDir, time.Now().Format("2006/01/02"), fmt.Sprintf("%d.log", cu.id))
|
||||
}
|
||||
|
||||
cu.logFile, err = util.TryOpen(cu.logPath, os.O_APPEND|os.O_CREATE|os.O_RDWR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cu *cmdUint) writeLog(b []byte) {
|
||||
if cu.ignoreFileLog {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
logPath := filepath.Join(cu.logDir, time.Now().Format("2006/01/02"), fmt.Sprintf("%d.log", cu.id))
|
||||
if logPath != cu.logPath {
|
||||
cu.logFile.Close()
|
||||
cu.logFile, err = util.TryOpen(logPath, os.O_APPEND|os.O_CREATE|os.O_RDWR)
|
||||
if err != nil {
|
||||
log.Errorf("writeLog failed - %v", err)
|
||||
return
|
||||
}
|
||||
cu.logPath = logPath
|
||||
}
|
||||
|
||||
cu.logFile.Write(b)
|
||||
}
|
||||
|
||||
func (cu *cmdUint) exec() error {
|
||||
log.Debug("cmd exec args:", cu.args)
|
||||
if len(cu.args) == 0 {
|
||||
return errors.New("invalid args")
|
||||
}
|
||||
cu.args[0] = util.FilterEmptyEle(cu.args[0])
|
||||
cmdName := cu.args[0][0]
|
||||
args := cu.args[0][1:]
|
||||
cmd := kproc.CommandContext(cu.ctx, cmdName, args...)
|
||||
cfg := cu.jd.getOpts()
|
||||
|
||||
cmd.SetDir(cu.dir)
|
||||
cmd.SetEnv(cu.env)
|
||||
cmd.SetUser(cu.user)
|
||||
cmd.SetExitKillChildProcess(cu.killChildProcess)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stdout.Close()
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stderr.Close()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stdout)
|
||||
readerErr := bufio.NewReader(stderr)
|
||||
// 如果已经存在日志则直接写入
|
||||
cu.writeLog(cu.content)
|
||||
go func() {
|
||||
var (
|
||||
line []byte
|
||||
)
|
||||
|
||||
for {
|
||||
|
||||
line, _ = reader.ReadBytes('\n')
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if cfg.VerboseJobLog {
|
||||
prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
|
||||
line = append([]byte(prefix), line...)
|
||||
}
|
||||
|
||||
if cu.exportLog {
|
||||
cu.content = append(cu.content, line...)
|
||||
}
|
||||
cu.writeLog(line)
|
||||
}
|
||||
|
||||
for {
|
||||
line, _ = readerErr.ReadBytes('\n')
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
// 默认给err信息加上日期标志
|
||||
if cfg.VerboseJobLog {
|
||||
prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
|
||||
line = append([]byte(prefix), line...)
|
||||
}
|
||||
if cu.exportLog {
|
||||
cu.content = append(cu.content, line...)
|
||||
}
|
||||
cu.writeLog(line)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cu *cmdUint) pipeExec() error {
|
||||
var (
|
||||
outBufer bytes.Buffer
|
||||
errBufer bytes.Buffer
|
||||
cmdEntryList []*pipeCmd
|
||||
err, exitError error
|
||||
line []byte
|
||||
cfg = cu.jd.getOpts()
|
||||
)
|
||||
|
||||
for _, v := range cu.args {
|
||||
v = util.FilterEmptyEle(v)
|
||||
cmdName := v[0]
|
||||
args := v[1:]
|
||||
|
||||
cmd := kproc.CommandContext(cu.ctx, cmdName, args...)
|
||||
|
||||
cmd.SetDir(cu.dir)
|
||||
cmd.SetEnv(cu.env)
|
||||
cmd.SetUser(cu.user)
|
||||
cmd.SetExitKillChildProcess(cu.killChildProcess)
|
||||
|
||||
cmdEntryList = append(cmdEntryList, &pipeCmd{cmd})
|
||||
}
|
||||
|
||||
exitError = execute(&outBufer, &errBufer,
|
||||
cmdEntryList...,
|
||||
)
|
||||
|
||||
// 如果已经存在日志则直接写入
|
||||
cu.writeLog(cu.content)
|
||||
|
||||
for {
|
||||
|
||||
line, err = outBufer.ReadBytes('\n')
|
||||
if err != nil || err == io.EOF {
|
||||
break
|
||||
}
|
||||
if cfg.VerboseJobLog {
|
||||
prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
|
||||
line = append([]byte(prefix), line...)
|
||||
}
|
||||
|
||||
cu.content = append(cu.content, line...)
|
||||
cu.writeLog(line)
|
||||
}
|
||||
|
||||
for {
|
||||
line, err = errBufer.ReadBytes('\n')
|
||||
if err != nil || err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if cfg.VerboseJobLog {
|
||||
prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
|
||||
line = append([]byte(prefix), line...)
|
||||
}
|
||||
|
||||
if cu.exportLog {
|
||||
cu.content = append(cu.content, line...)
|
||||
}
|
||||
cu.writeLog(line)
|
||||
}
|
||||
return exitError
|
||||
}
|
||||
|
||||
func call(stack []*pipeCmd, pipes []*io.PipeWriter) (err error) {
|
||||
if stack[0].Process == nil {
|
||||
if err = stack[0].Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(stack) > 1 {
|
||||
if err = stack[1].Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
pipes[0].Close()
|
||||
if err == nil {
|
||||
err = call(stack[1:], pipes[1:])
|
||||
}
|
||||
if err != nil {
|
||||
// fixed zombie process
|
||||
stack[1].Wait()
|
||||
}
|
||||
}()
|
||||
}
|
||||
return stack[0].Wait()
|
||||
}
|
||||
|
||||
type pipeCmd struct {
|
||||
*kproc.KCmd
|
||||
}
|
||||
|
||||
func execute(outputBuffer *bytes.Buffer, errorBuffer *bytes.Buffer, stack ...*pipeCmd) (err error) {
|
||||
pipeStack := make([]*io.PipeWriter, len(stack)-1)
|
||||
i := 0
|
||||
for ; i < len(stack)-1; i++ {
|
||||
stdinPipe, stdoutPipe := io.Pipe()
|
||||
stack[i].Stdout = stdoutPipe
|
||||
stack[i].Stderr = errorBuffer
|
||||
stack[i+1].Stdin = stdinPipe
|
||||
pipeStack[i] = stdoutPipe
|
||||
}
|
||||
|
||||
stack[i].Stdout = outputBuffer
|
||||
stack[i].Stderr = errorBuffer
|
||||
|
||||
if err = call(stack, pipeStack); err != nil {
|
||||
errorBuffer.WriteString(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
110
jiacrontabd/config.go
Normal file
110
jiacrontabd/config.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/file"
|
||||
"jiacrontab/pkg/util"
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
appname = "jiacrontabd"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `opt:"log_level"`
|
||||
VerboseJobLog bool `opt:"verbose_job_log"`
|
||||
ListenAddr string `opt:"listen_addr"`
|
||||
AdminAddr string `opt:"admin_addr"`
|
||||
LogPath string `opt:"log_path"`
|
||||
AutoCleanTaskLog bool `opt:"auto_clean_task_log"`
|
||||
NodeName string `opt:"node_name"`
|
||||
BoardcastAddr string `opt:"boardcast_addr"`
|
||||
ClientAliveInterval int `opt:"client_alive_interval"`
|
||||
CfgPath string
|
||||
Debug bool `opt:"debug"`
|
||||
iniFile *ini.File
|
||||
DriverName string `opt:"driver_name"`
|
||||
DSN string `opt:"dsn"`
|
||||
}
|
||||
|
||||
func (c *Config) Resolve() error {
|
||||
c.iniFile = c.loadConfig(c.CfgPath)
|
||||
|
||||
val := reflect.ValueOf(c).Elem()
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
opt := field.Tag.Get("opt")
|
||||
if opt == "" {
|
||||
continue
|
||||
}
|
||||
sec := c.iniFile.Section("jiacrontabd")
|
||||
|
||||
if !sec.HasKey(opt) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := sec.Key(opt)
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Bool:
|
||||
v, err := key.Bool()
|
||||
if err != nil {
|
||||
log.Errorf("cannot resolve ini field %s err(%v)", opt, err)
|
||||
}
|
||||
val.Field(i).SetBool(v)
|
||||
case reflect.String:
|
||||
val.Field(i).SetString(key.String())
|
||||
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int8:
|
||||
if v, err := key.Int64(); err != nil {
|
||||
log.Errorf("cannot convert to int64 type (%s)", err)
|
||||
} else {
|
||||
val.Field(i).SetInt(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.BoardcastAddr == "" {
|
||||
_, port, _ := net.SplitHostPort(c.ListenAddr)
|
||||
c.BoardcastAddr = util.InternalIP() + ":" + port
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
LogLevel: "warn",
|
||||
VerboseJobLog: true,
|
||||
ListenAddr: "127.0.0.1:20001",
|
||||
AdminAddr: "127.0.0.1:20003",
|
||||
LogPath: "./logs",
|
||||
AutoCleanTaskLog: true,
|
||||
NodeName: util.GetHostname(),
|
||||
CfgPath: "./jiacrontabd.ini",
|
||||
DriverName: "sqlite3",
|
||||
DSN: "data/jiacrontabd.db",
|
||||
ClientAliveInterval: 30,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) loadConfig(path string) *ini.File {
|
||||
if !file.Exist(path) {
|
||||
f, err := file.CreateFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
iniFile, err := ini.Load(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return iniFile
|
||||
}
|
||||
1
jiacrontabd/const.go
Normal file
1
jiacrontabd/const.go
Normal file
@@ -0,0 +1 @@
|
||||
package jiacrontabd
|
||||
275
jiacrontabd/daemon.go
Normal file
275
jiacrontabd/daemon.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/proto"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
type ApiNotifyArgs struct {
|
||||
JobName string
|
||||
JobID uint
|
||||
NodeAddr string
|
||||
CreateUsername string
|
||||
CreatedAt time.Time
|
||||
NotifyType string
|
||||
}
|
||||
|
||||
type daemonJob struct {
|
||||
job *models.DaemonJob
|
||||
daemon *Daemon
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
processNum int
|
||||
}
|
||||
|
||||
func (d *daemonJob) do(ctx context.Context) {
|
||||
|
||||
d.processNum = 1
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
defer t.Stop()
|
||||
d.daemon.wait.Add(1)
|
||||
cfg := d.daemon.jd.getOpts()
|
||||
retryNum := d.job.RetryNum
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Errorf("%s exec panic %s \n", d.job.Name, err)
|
||||
}
|
||||
d.processNum = 0
|
||||
if err := models.DB().Model(d.job).Update("status", models.StatusJobStop).Error; err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
d.daemon.wait.Done()
|
||||
|
||||
}()
|
||||
|
||||
if err := models.DB().Model(d.job).Updates(map[string]interface{}{
|
||||
"start_at": time.Now(),
|
||||
"status": models.StatusJobRunning}).Error; err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
var (
|
||||
stop bool
|
||||
err error
|
||||
)
|
||||
arg := d.job.Command
|
||||
if d.job.Code != "" {
|
||||
arg = append(arg, d.job.Code)
|
||||
}
|
||||
myCmdUint := cmdUint{
|
||||
ctx: ctx,
|
||||
args: [][]string{arg},
|
||||
env: d.job.WorkEnv,
|
||||
ip: d.job.WorkIp,
|
||||
dir: d.job.WorkDir,
|
||||
user: d.job.WorkUser,
|
||||
label: d.job.Name,
|
||||
jd: d.daemon.jd,
|
||||
id: d.job.ID,
|
||||
logDir: filepath.Join(cfg.LogPath, "daemon_job"),
|
||||
}
|
||||
|
||||
log.Info("exec daemon job, jobName:", d.job.Name, " jobID", d.job.ID)
|
||||
|
||||
err = myCmdUint.launch()
|
||||
retryNum--
|
||||
d.handleNotify(err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stop = true
|
||||
case <-t.C:
|
||||
}
|
||||
|
||||
if stop || d.job.FailRestart == false || (d.job.RetryNum > 0 && retryNum == 0) {
|
||||
break
|
||||
}
|
||||
|
||||
if err = d.syncJob(); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
t.Stop()
|
||||
|
||||
d.daemon.PopJob(d.job.ID)
|
||||
|
||||
log.Info("daemon task end", d.job.Name)
|
||||
}
|
||||
|
||||
func (d *daemonJob) syncJob() error {
|
||||
return models.DB().Take(d.job, "id=? and status=?", d.job.ID, models.StatusJobRunning).Error
|
||||
}
|
||||
|
||||
func (d *daemonJob) handleNotify(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var reply bool
|
||||
cfg := d.daemon.jd.getOpts()
|
||||
if d.job.ErrorMailNotify && len(d.job.MailTo) > 0 {
|
||||
var reply bool
|
||||
err := d.daemon.jd.rpcCallCtx(d.ctx, "Srv.SendMail", proto.SendMail{
|
||||
MailTo: d.job.MailTo,
|
||||
Subject: cfg.BoardcastAddr + "提醒常驻脚本异常退出",
|
||||
Content: fmt.Sprintf(
|
||||
"任务名:%s<br/>创建者:%s<br/>开始时间:%s<br/>异常:%s",
|
||||
d.job.Name, d.job.CreatedUsername, time.Now().Format(proto.DefaultTimeLayout), err),
|
||||
}, &reply)
|
||||
if err != nil {
|
||||
log.Error("Srv.SendMail error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if d.job.ErrorAPINotify && len(d.job.APITo) > 0 {
|
||||
postData, err := json.Marshal(ApiNotifyArgs{
|
||||
JobName: d.job.Name,
|
||||
JobID: d.job.ID,
|
||||
CreateUsername: d.job.CreatedUsername,
|
||||
CreatedAt: d.job.CreatedAt,
|
||||
NodeAddr: cfg.BoardcastAddr,
|
||||
NotifyType: "error",
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("json.Marshal error:", err)
|
||||
}
|
||||
err = d.daemon.jd.rpcCallCtx(d.ctx, "Srv.ApiPost", proto.ApiPost{
|
||||
Urls: d.job.APITo,
|
||||
Data: string(postData),
|
||||
}, &reply)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Logic.ApiPost error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// 钉钉webhook通知
|
||||
if d.job.ErrorDingdingNotify && len(d.job.DingdingTo) > 0 {
|
||||
nodeAddr := cfg.BoardcastAddr
|
||||
title := nodeAddr + "告警:常驻脚本异常退出"
|
||||
notifyContent := fmt.Sprintf("> ###### 来自jiacrontabd: %s 的常驻脚本异常退出报警:\n> ##### 任务id:%d\n> ##### 任务名称:%s\n> ##### 异常:%s\n> ##### 报警时间:%s", nodeAddr, int(d.job.ID), d.job.Name, err, time.Now().Format("2006-01-02 15:04:05"))
|
||||
notifyBody := fmt.Sprintf(
|
||||
`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "%s",
|
||||
"text": "%s"
|
||||
}
|
||||
}`, title, notifyContent)
|
||||
err = d.daemon.jd.rpcCallCtx(d.ctx, "Srv.ApiPost", proto.ApiPost{
|
||||
Urls: d.job.DingdingTo,
|
||||
Data: notifyBody,
|
||||
}, &reply)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Logic.ApiPost error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Daemon struct {
|
||||
taskChannel chan *daemonJob
|
||||
taskMap map[uint]*daemonJob
|
||||
jd *Jiacrontabd
|
||||
lock sync.Mutex
|
||||
wait sync.WaitGroup
|
||||
}
|
||||
|
||||
func newDaemon(taskChannelLength int, jd *Jiacrontabd) *Daemon {
|
||||
return &Daemon{
|
||||
taskMap: make(map[uint]*daemonJob),
|
||||
taskChannel: make(chan *daemonJob, taskChannelLength),
|
||||
jd: jd,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Daemon) add(t *daemonJob) {
|
||||
if t != nil {
|
||||
if len(t.job.WorkIp) > 0 && !checkIpInWhiteList(strings.Join(t.job.WorkIp, ",")) {
|
||||
if err := models.DB().Model(t.job).Updates(map[string]interface{}{
|
||||
"status": models.StatusJobStop,
|
||||
//"next_exec_time": time.Time{},
|
||||
//"last_exit_status": "IP受限制",
|
||||
}).Error; err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("daemon.add(%s)\n", t.job.Name)
|
||||
t.daemon = d
|
||||
d.taskChannel <- t
|
||||
}
|
||||
}
|
||||
|
||||
// PopJob 删除调度列表中的任务
|
||||
func (d *Daemon) PopJob(jobID uint) {
|
||||
d.lock.Lock()
|
||||
t := d.taskMap[jobID]
|
||||
if t != nil {
|
||||
delete(d.taskMap, jobID)
|
||||
d.lock.Unlock()
|
||||
t.cancel()
|
||||
} else {
|
||||
d.lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Daemon) run() {
|
||||
var jobList []models.DaemonJob
|
||||
err := models.DB().Where("status=?", models.StatusJobRunning).Find(&jobList).Error
|
||||
if err != nil {
|
||||
log.Error("init daemon task error:", err)
|
||||
}
|
||||
|
||||
for _, v := range jobList {
|
||||
job := v
|
||||
d.add(&daemonJob{
|
||||
job: &job,
|
||||
})
|
||||
}
|
||||
|
||||
d.process()
|
||||
}
|
||||
|
||||
func (d *Daemon) process() {
|
||||
go func() {
|
||||
for v := range d.taskChannel {
|
||||
d.lock.Lock()
|
||||
if t := d.taskMap[v.job.ID]; t == nil {
|
||||
d.taskMap[v.job.ID] = v
|
||||
d.lock.Unlock()
|
||||
v.ctx, v.cancel = context.WithCancel(context.Background())
|
||||
go v.do(v.ctx)
|
||||
} else {
|
||||
d.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *Daemon) count() int {
|
||||
var count int
|
||||
d.lock.Lock()
|
||||
count = len(d.taskMap)
|
||||
d.lock.Unlock()
|
||||
return count
|
||||
}
|
||||
|
||||
func (d *Daemon) waitDone() {
|
||||
d.wait.Wait()
|
||||
}
|
||||
122
jiacrontabd/dependencies.go
Normal file
122
jiacrontabd/dependencies.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"jiacrontab/pkg/proto"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
type depEntry struct {
|
||||
jobID uint // 定时任务id
|
||||
jobUniqueID string // job的唯一标志
|
||||
processID int // 当前依赖的父级任务(可能存在多个并发的task)
|
||||
id string // depID uuid
|
||||
once bool
|
||||
workDir string
|
||||
user string
|
||||
env []string
|
||||
from string
|
||||
commands []string
|
||||
dest string
|
||||
done bool
|
||||
timeout int64
|
||||
err error
|
||||
ctx context.Context
|
||||
name string
|
||||
logPath string
|
||||
logContent []byte
|
||||
}
|
||||
|
||||
func newDependencies(jd *Jiacrontabd) *dependencies {
|
||||
return &dependencies{
|
||||
jd: jd,
|
||||
dep: make(chan *depEntry, 100),
|
||||
}
|
||||
}
|
||||
|
||||
type dependencies struct {
|
||||
jd *Jiacrontabd
|
||||
dep chan *depEntry
|
||||
}
|
||||
|
||||
func (d *dependencies) add(t *depEntry) {
|
||||
select {
|
||||
case d.dep <- t:
|
||||
default:
|
||||
log.Warnf("discard %v", t)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *dependencies) run() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case t := <-d.dep:
|
||||
go d.exec(t)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// TODO: 主任务退出杀死依赖
|
||||
func (d *dependencies) exec(task *depEntry) {
|
||||
|
||||
var (
|
||||
reply bool
|
||||
err error
|
||||
)
|
||||
|
||||
if task.timeout == 0 {
|
||||
// 默认超时10分钟
|
||||
task.timeout = 600
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(task.timeout)*time.Second)
|
||||
defer cancel()
|
||||
myCmdUnit := cmdUint{
|
||||
args: [][]string{task.commands},
|
||||
ctx: ctx,
|
||||
dir: task.workDir,
|
||||
user: task.user,
|
||||
logPath: task.logPath,
|
||||
ignoreFileLog: true,
|
||||
jd: d.jd,
|
||||
exportLog: true,
|
||||
}
|
||||
|
||||
log.Infof("dep start exec %s->%v", task.name, task.commands)
|
||||
task.err = myCmdUnit.launch()
|
||||
task.logContent = bytes.TrimRight(myCmdUnit.content, "\x00")
|
||||
task.done = true
|
||||
log.Infof("exec %s %s cost %.4fs %v", task.name, task.commands, float64(myCmdUnit.costTime)/1000000000, err)
|
||||
|
||||
task.dest, task.from = task.from, task.dest
|
||||
|
||||
if !d.jd.SetDependDone(task) {
|
||||
err = d.jd.rpcCallCtx(ctx, "Srv.SetDependDone", proto.DepJob{
|
||||
Name: task.name,
|
||||
Dest: task.dest,
|
||||
From: task.from,
|
||||
ID: task.id,
|
||||
JobUniqueID: task.jobUniqueID,
|
||||
ProcessID: task.processID,
|
||||
JobID: task.jobID,
|
||||
Commands: task.commands,
|
||||
LogContent: task.logContent,
|
||||
Err: err,
|
||||
Timeout: task.timeout,
|
||||
}, &reply)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Srv.SetDependDone error:", err, "server addr:", d.jd.getOpts().AdminAddr)
|
||||
}
|
||||
|
||||
if !reply {
|
||||
log.Errorf("task %s %v call Srv.SetDependDone failed! err:%v", task.name, task.commands, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
489
jiacrontabd/jiacrontabd.go
Normal file
489
jiacrontabd/jiacrontabd.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/crontab"
|
||||
"jiacrontab/pkg/finder"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/rpc"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
"jiacrontab/pkg/util"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Jiacrontabd scheduling center
|
||||
type Jiacrontabd struct {
|
||||
crontab *crontab.Crontab
|
||||
// All jobs added
|
||||
jobs map[uint]*JobEntry
|
||||
tmpJobs map[string]*JobEntry
|
||||
dep *dependencies
|
||||
daemon *Daemon
|
||||
heartbeatPeriod time.Duration
|
||||
mux sync.RWMutex
|
||||
startTime time.Time
|
||||
cfg atomic.Value
|
||||
wg util.WaitGroupWrapper
|
||||
}
|
||||
|
||||
// New return a Jiacrontabd instance
|
||||
func New(opt *Config) *Jiacrontabd {
|
||||
j := &Jiacrontabd{
|
||||
jobs: make(map[uint]*JobEntry),
|
||||
tmpJobs: make(map[string]*JobEntry),
|
||||
|
||||
heartbeatPeriod: 5 * time.Second,
|
||||
crontab: crontab.New(),
|
||||
}
|
||||
j.swapOpts(opt)
|
||||
j.dep = newDependencies(j)
|
||||
j.daemon = newDaemon(100, j)
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) getOpts() *Config {
|
||||
return j.cfg.Load().(*Config)
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) swapOpts(opts *Config) {
|
||||
j.cfg.Store(opts)
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) addTmpJob(job *JobEntry) {
|
||||
j.mux.Lock()
|
||||
j.tmpJobs[job.uniqueID] = job
|
||||
j.mux.Unlock()
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) removeTmpJob(job *JobEntry) {
|
||||
j.mux.Lock()
|
||||
delete(j.tmpJobs, job.uniqueID)
|
||||
j.mux.Unlock()
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) addJob(job *crontab.Job, updateLastExecTime bool) error {
|
||||
j.mux.Lock()
|
||||
if v, ok := j.jobs[job.ID]; ok {
|
||||
v.job = job
|
||||
} else {
|
||||
var crontabJob models.CrontabJob
|
||||
err := models.DB().First(&crontabJob, "id=?", job.ID).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
j.mux.Unlock()
|
||||
return nil
|
||||
}
|
||||
if len(crontabJob.WorkIp) > 0 && !checkIpInWhiteList(strings.Join(crontabJob.WorkIp, ",")) {
|
||||
if err := models.DB().Model(&models.CrontabJob{}).Where("id=?", job.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"status": models.StatusJobStop,
|
||||
"next_exec_time": time.Time{},
|
||||
"last_exit_status": "IP受限制",
|
||||
}).Error; err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
j.mux.Unlock()
|
||||
return nil
|
||||
}
|
||||
j.jobs[job.ID] = newJobEntry(job, j)
|
||||
}
|
||||
j.mux.Unlock()
|
||||
|
||||
if err := j.crontab.AddJob(job); err != nil {
|
||||
log.Error("NextExecutionTime:", err, " timeArgs:", job)
|
||||
return fmt.Errorf("时间格式错误: %v - %s", err, job.Format())
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"next_exec_time": job.GetNextExecTime(),
|
||||
"status": models.StatusJobTiming,
|
||||
}
|
||||
|
||||
if updateLastExecTime {
|
||||
data["last_exec_time"] = time.Now()
|
||||
}
|
||||
|
||||
if err := models.DB().Model(&models.CrontabJob{}).Where("id=?", job.ID).
|
||||
Updates(data).Error; err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) execTask(job *crontab.Job) {
|
||||
|
||||
j.mux.RLock()
|
||||
if task, ok := j.jobs[job.ID]; ok {
|
||||
j.mux.RUnlock()
|
||||
task.exec()
|
||||
return
|
||||
}
|
||||
log.Warnf("not found jobID(%d)", job.ID)
|
||||
j.mux.RUnlock()
|
||||
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) killTask(jobID uint) {
|
||||
var jobs []*JobEntry
|
||||
j.mux.RLock()
|
||||
if job, ok := j.jobs[jobID]; ok {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
for _, v := range j.tmpJobs {
|
||||
if v.detail.ID == jobID {
|
||||
jobs = append(jobs, v)
|
||||
}
|
||||
}
|
||||
j.mux.RUnlock()
|
||||
|
||||
for _, v := range jobs {
|
||||
v.kill()
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) run() {
|
||||
j.dep.run()
|
||||
j.daemon.run()
|
||||
j.wg.Wrap(j.crontab.QueueScanWorker)
|
||||
|
||||
for v := range j.crontab.Ready() {
|
||||
v := v.Value.(*crontab.Job)
|
||||
j.execTask(v)
|
||||
}
|
||||
}
|
||||
|
||||
// SetDependDone 依赖执行完毕时设置相关状态
|
||||
// 目标网络不是本机时返回false
|
||||
func (j *Jiacrontabd) SetDependDone(task *depEntry) bool {
|
||||
|
||||
var (
|
||||
ok bool
|
||||
job *JobEntry
|
||||
)
|
||||
|
||||
if task.dest != j.getOpts().BoardcastAddr {
|
||||
return false
|
||||
}
|
||||
|
||||
isAllDone := true
|
||||
|
||||
j.mux.Lock()
|
||||
if job, ok = j.tmpJobs[task.jobUniqueID]; !ok {
|
||||
job, ok = j.jobs[task.jobID]
|
||||
}
|
||||
j.mux.Unlock()
|
||||
if ok {
|
||||
|
||||
var logContent []byte
|
||||
var curTaskEntry *process
|
||||
|
||||
for _, p := range job.processes {
|
||||
if int(p.id) == task.processID {
|
||||
curTaskEntry = p
|
||||
for _, dep := range p.deps {
|
||||
|
||||
if dep.id == task.id {
|
||||
dep.dest = task.dest
|
||||
dep.from = task.from
|
||||
dep.logContent = task.logContent
|
||||
dep.err = task.err
|
||||
dep.done = true
|
||||
}
|
||||
|
||||
if dep.done == false {
|
||||
isAllDone = false
|
||||
} else {
|
||||
logContent = append(logContent, dep.logContent...)
|
||||
}
|
||||
// 同步模式上一个依赖结束才会触发下一个
|
||||
if dep.id == task.id && task.err == nil && p.jobEntry.detail.IsSync {
|
||||
if err := j.dispatchDependSync(p.ctx, p.deps, dep.id); err != nil {
|
||||
task.err = err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if curTaskEntry == nil {
|
||||
log.Infof("cannot find task entry %s %s", task.name, task.commands)
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果依赖任务执行出错直接通知主任务停止
|
||||
if task.err != nil {
|
||||
isAllDone = true
|
||||
curTaskEntry.err = task.err
|
||||
log.Infof("depend %s %s exec failed, %s, try to stop master task", task.name, task.commands, task.err)
|
||||
}
|
||||
|
||||
if isAllDone {
|
||||
curTaskEntry.ready <- struct{}{}
|
||||
curTaskEntry.jobEntry.logContent = append(curTaskEntry.jobEntry.logContent, logContent...)
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Infof("cannot find task handler %s %s", task.name, task.commands)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// 同步模式根据depEntryID确定位置实现任务的依次调度
|
||||
func (j *Jiacrontabd) dispatchDependSync(ctx context.Context, deps []*depEntry, depEntryID string) error {
|
||||
flag := true
|
||||
cfg := j.getOpts()
|
||||
if len(deps) > 0 {
|
||||
flag = false
|
||||
for _, v := range deps {
|
||||
// 根据flag实现调度下一个依赖任务
|
||||
if flag || depEntryID == "" {
|
||||
// 检测目标服务器为本机时直接执行脚本
|
||||
if v.dest == cfg.BoardcastAddr {
|
||||
j.dep.add(v)
|
||||
} else {
|
||||
var reply bool
|
||||
err := j.rpcCallCtx(ctx, "Srv.ExecDepend", []proto.DepJob{{
|
||||
ID: v.id,
|
||||
Name: v.name,
|
||||
Dest: v.dest,
|
||||
From: v.from,
|
||||
JobUniqueID: v.jobUniqueID,
|
||||
JobID: v.jobID,
|
||||
ProcessID: v.processID,
|
||||
Commands: v.commands,
|
||||
Timeout: v.timeout,
|
||||
}}, &reply)
|
||||
if !reply || err != nil {
|
||||
return fmt.Errorf("Srv.ExecDepend error:%v server addr:%s", err, cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if v.id == depEntryID {
|
||||
flag = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) dispatchDependAsync(ctx context.Context, deps []*depEntry) error {
|
||||
var depJobs proto.DepJobs
|
||||
cfg := j.getOpts()
|
||||
for _, v := range deps {
|
||||
// 检测目标服务器是本机直接执行脚本
|
||||
if v.dest == cfg.BoardcastAddr {
|
||||
j.dep.add(v)
|
||||
} else {
|
||||
depJobs = append(depJobs, proto.DepJob{
|
||||
ID: v.id,
|
||||
Name: v.name,
|
||||
Dest: v.dest,
|
||||
From: v.from,
|
||||
ProcessID: v.processID,
|
||||
JobID: v.jobID,
|
||||
JobUniqueID: v.jobUniqueID,
|
||||
Commands: v.commands,
|
||||
Timeout: v.timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(depJobs) > 0 {
|
||||
var reply bool
|
||||
if err := j.rpcCallCtx(ctx, "Srv.ExecDepend", depJobs, &reply); err != nil {
|
||||
return fmt.Errorf("Srv.ExecDepend error:%v server addr: %s", err, cfg.AdminAddr)
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) count() int {
|
||||
j.mux.RLock()
|
||||
num := len(j.jobs)
|
||||
j.mux.RUnlock()
|
||||
return num
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) deleteJob(jobID uint) {
|
||||
j.mux.Lock()
|
||||
delete(j.jobs, jobID)
|
||||
j.mux.Unlock()
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) heartBeat() {
|
||||
var (
|
||||
reply bool
|
||||
cronJobs []struct {
|
||||
Total uint
|
||||
GroupID uint
|
||||
Failed bool
|
||||
Status models.JobStatus
|
||||
}
|
||||
daemonJobs []struct {
|
||||
Total uint
|
||||
GroupID uint
|
||||
Status models.JobStatus
|
||||
}
|
||||
ok bool
|
||||
nodes = make(map[uint]models.Node)
|
||||
cfg = j.getOpts()
|
||||
nodeName = cfg.NodeName
|
||||
node models.Node
|
||||
superGroupNode models.Node
|
||||
)
|
||||
|
||||
if nodeName == "" {
|
||||
nodeName = util.GetHostname()
|
||||
}
|
||||
|
||||
models.DB().Model(&models.CrontabJob{}).Select("id,group_id,status,failed,count(1) as total").Group("group_id,status,failed").Scan(&cronJobs)
|
||||
models.DB().Model(&models.DaemonJob{}).Select("id,group_id,status,count(1) as total").Group("group_id,status").Scan(&daemonJobs)
|
||||
|
||||
nodes[models.SuperGroup.ID] = models.Node{
|
||||
Addr: cfg.BoardcastAddr,
|
||||
GroupID: models.SuperGroup.ID,
|
||||
Name: nodeName,
|
||||
}
|
||||
|
||||
for _, job := range cronJobs {
|
||||
superGroupNode = nodes[models.SuperGroup.ID]
|
||||
if node, ok = nodes[job.GroupID]; !ok {
|
||||
node = models.Node{
|
||||
Addr: cfg.BoardcastAddr,
|
||||
GroupID: job.GroupID,
|
||||
Name: nodeName,
|
||||
}
|
||||
}
|
||||
|
||||
if job.Failed && (job.Status == models.StatusJobTiming || job.Status == models.StatusJobRunning) {
|
||||
node.CrontabJobFailNum += job.Total
|
||||
superGroupNode.CrontabJobFailNum += job.Total
|
||||
}
|
||||
|
||||
if job.Status == models.StatusJobUnaudited {
|
||||
node.CrontabJobAuditNum += job.Total
|
||||
superGroupNode.CrontabJobAuditNum += job.Total
|
||||
}
|
||||
|
||||
if job.Status == models.StatusJobTiming || job.Status == models.StatusJobRunning {
|
||||
node.CrontabTaskNum += job.Total
|
||||
superGroupNode.CrontabTaskNum += job.Total
|
||||
}
|
||||
|
||||
nodes[job.GroupID] = node
|
||||
nodes[models.SuperGroup.ID] = superGroupNode
|
||||
}
|
||||
|
||||
for _, job := range daemonJobs {
|
||||
superGroupNode = nodes[models.SuperGroup.ID]
|
||||
if node, ok = nodes[job.GroupID]; !ok {
|
||||
node = models.Node{
|
||||
Addr: cfg.BoardcastAddr,
|
||||
GroupID: job.GroupID,
|
||||
Name: nodeName,
|
||||
}
|
||||
}
|
||||
if job.Status == models.StatusJobUnaudited {
|
||||
node.DaemonJobAuditNum += job.Total
|
||||
superGroupNode.DaemonJobAuditNum += job.Total
|
||||
}
|
||||
if job.Status == models.StatusJobRunning {
|
||||
node.DaemonTaskNum += job.Total
|
||||
superGroupNode.DaemonTaskNum += job.Total
|
||||
}
|
||||
nodes[job.GroupID] = node
|
||||
nodes[models.SuperGroup.ID] = superGroupNode
|
||||
}
|
||||
|
||||
err := j.rpcCallCtx(context.TODO(), rpc.RegisterService, nodes, &reply)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Srv.Register error:", err, ",server addr:", cfg.AdminAddr)
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Duration(j.getOpts().ClientAliveInterval)*time.Second, j.heartBeat)
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) recovery() {
|
||||
var crontabJobs []models.CrontabJob
|
||||
var daemonJobs []models.DaemonJob
|
||||
|
||||
// reset processNUm 0
|
||||
err := models.DB().Model(&models.CrontabJob{}).Where("process_num > ?", 0).Update("process_num", 0).Error
|
||||
if err != nil {
|
||||
log.Debug("crontab recovery: reset processNum failed -", err)
|
||||
}
|
||||
|
||||
err = models.DB().Find(&crontabJobs, "status IN (?)", []models.JobStatus{models.StatusJobTiming, models.StatusJobRunning}).Error
|
||||
if err != nil {
|
||||
log.Debug("crontab recovery:", err)
|
||||
}
|
||||
|
||||
for _, v := range crontabJobs {
|
||||
j.addJob(&crontab.Job{
|
||||
ID: v.ID,
|
||||
Second: v.TimeArgs.Second,
|
||||
Minute: v.TimeArgs.Minute,
|
||||
Hour: v.TimeArgs.Hour,
|
||||
Day: v.TimeArgs.Day,
|
||||
Month: v.TimeArgs.Month,
|
||||
Weekday: v.TimeArgs.Weekday,
|
||||
}, false)
|
||||
}
|
||||
|
||||
err = models.DB().Find(&daemonJobs, "status in (?)", []models.JobStatus{models.StatusJobOk}).Error
|
||||
|
||||
if err != nil {
|
||||
log.Debug("daemon recovery:", err)
|
||||
}
|
||||
|
||||
for _, v := range daemonJobs {
|
||||
job := v
|
||||
j.daemon.add(&daemonJob{
|
||||
job: &job,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) init() {
|
||||
cfg := j.getOpts()
|
||||
if err := models.CreateDB(cfg.DriverName, cfg.DSN); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
models.DB().AutoMigrate(&models.CrontabJob{}, &models.DaemonJob{})
|
||||
j.startTime = time.Now()
|
||||
if cfg.AutoCleanTaskLog {
|
||||
go finder.SearchAndDeleteFileOnDisk(cfg.LogPath, 24*time.Hour*30, 1<<30)
|
||||
}
|
||||
j.recovery()
|
||||
}
|
||||
|
||||
func (j *Jiacrontabd) rpcCallCtx(ctx context.Context, serviceMethod string, args, reply interface{}) error {
|
||||
return rpc.CallCtx(j.getOpts().AdminAddr, serviceMethod, ctx, args, reply)
|
||||
}
|
||||
|
||||
// Main main function
|
||||
func (j *Jiacrontabd) Main() {
|
||||
j.init()
|
||||
j.heartBeat()
|
||||
go j.run()
|
||||
rpc.ListenAndServe(j.getOpts().ListenAddr, newCrontabJobSrv(j), newDaemonJobSrv(j), newSrv(j))
|
||||
}
|
||||
604
jiacrontabd/job.go
Normal file
604
jiacrontabd/job.go
Normal file
@@ -0,0 +1,604 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/crontab"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/util"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
const (
|
||||
exitError = "Error"
|
||||
exitKilled = "Killed"
|
||||
exitSuccess = "Success"
|
||||
exitDependError = "Dependent job execution failed"
|
||||
exitTimeout = "Timeout"
|
||||
)
|
||||
|
||||
type process struct {
|
||||
id uint32
|
||||
deps []*depEntry
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
err error
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
ready chan struct{}
|
||||
retryNum int
|
||||
jobEntry *JobEntry
|
||||
}
|
||||
|
||||
func newProcess(id uint32, jobEntry *JobEntry) *process {
|
||||
p := &process{
|
||||
id: id,
|
||||
jobEntry: jobEntry,
|
||||
startTime: time.Now(),
|
||||
ready: make(chan struct{}),
|
||||
}
|
||||
|
||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||
|
||||
for _, v := range p.jobEntry.detail.DependJobs {
|
||||
cmd := v.Command
|
||||
if v.Code != "" {
|
||||
cmd = append(cmd, v.Code)
|
||||
}
|
||||
p.deps = append(p.deps, &depEntry{
|
||||
jobID: p.jobEntry.detail.ID,
|
||||
processID: int(id),
|
||||
jobUniqueID: p.jobEntry.uniqueID,
|
||||
id: v.ID,
|
||||
from: v.From,
|
||||
commands: cmd,
|
||||
dest: v.Dest,
|
||||
logPath: filepath.Join(p.jobEntry.jd.getOpts().LogPath, "depend_job", time.Now().Format("2006/01/02"), fmt.Sprintf("%d-%s.log", v.JobID, v.ID)),
|
||||
done: false,
|
||||
timeout: v.Timeout,
|
||||
})
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *process) waitDepExecDone() bool {
|
||||
|
||||
var err error
|
||||
|
||||
if len(p.deps) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if p.jobEntry.detail.IsSync {
|
||||
// 同步
|
||||
err = p.jobEntry.jd.dispatchDependSync(p.ctx, p.deps, "")
|
||||
} else {
|
||||
// 并发模式
|
||||
err = p.jobEntry.jd.dispatchDependAsync(p.ctx, p.deps)
|
||||
}
|
||||
if err != nil {
|
||||
prefix := fmt.Sprintf("[%s %s] ", time.Now().Format("2006-01-02 15:04:05"), p.jobEntry.jd.getOpts().BoardcastAddr)
|
||||
p.jobEntry.logContent = append(p.jobEntry.logContent, []byte(prefix+"failed to exec depends\n")...)
|
||||
return false
|
||||
}
|
||||
|
||||
c := time.NewTimer(3600 * time.Second)
|
||||
defer c.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
log.Debugf("jobID:%d exec cancel", p.jobEntry.detail.ID)
|
||||
return false
|
||||
case <-c.C:
|
||||
p.cancel()
|
||||
log.Errorf("jobID:%d exec dep timeout!", p.jobEntry.detail.ID)
|
||||
return false
|
||||
case <-p.ready:
|
||||
if p.err != nil {
|
||||
log.Errorf("jobID:%d exec dep error(%s)", p.jobEntry.detail.ID, p.err)
|
||||
return false
|
||||
}
|
||||
log.Debugf("jobID:%d exec all dep done.", p.jobEntry.detail.ID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *process) exec() error {
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
doneChan = make(chan struct{}, 1)
|
||||
)
|
||||
|
||||
if ok = p.waitDepExecDone(); !ok {
|
||||
p.jobEntry.handleDepError(p.startTime, p)
|
||||
} else {
|
||||
if p.jobEntry.detail.Timeout != 0 {
|
||||
time.AfterFunc(
|
||||
time.Duration(p.jobEntry.detail.Timeout)*time.Second, func() {
|
||||
select {
|
||||
case <-doneChan:
|
||||
close(doneChan)
|
||||
default:
|
||||
log.Debug("timeout callback:", "jobID:", p.jobEntry.detail.ID)
|
||||
p.jobEntry.timeoutTrigger(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
arg := p.jobEntry.detail.Command
|
||||
if p.jobEntry.detail.Code != "" {
|
||||
arg = append(arg, p.jobEntry.detail.Code)
|
||||
}
|
||||
|
||||
myCmdUnit := cmdUint{
|
||||
args: [][]string{arg},
|
||||
ctx: p.ctx,
|
||||
dir: p.jobEntry.detail.WorkDir,
|
||||
user: p.jobEntry.detail.WorkUser,
|
||||
env: p.jobEntry.detail.WorkEnv,
|
||||
ip: p.jobEntry.detail.WorkIp,
|
||||
content: p.jobEntry.logContent,
|
||||
logDir: p.jobEntry.getLogDir(),
|
||||
id: p.jobEntry.job.ID,
|
||||
label: p.jobEntry.detail.Name,
|
||||
killChildProcess: p.jobEntry.detail.KillChildProcess,
|
||||
jd: p.jobEntry.jd,
|
||||
}
|
||||
|
||||
if p.jobEntry.once {
|
||||
myCmdUnit.exportLog = true
|
||||
}
|
||||
p.err = myCmdUnit.launch()
|
||||
p.jobEntry.logContent = myCmdUnit.content
|
||||
doneChan <- struct{}{}
|
||||
|
||||
if p.err != nil {
|
||||
p.jobEntry.handleNotify(p)
|
||||
}
|
||||
}
|
||||
|
||||
p.endTime = time.Now()
|
||||
p.jobEntry.detail.LastCostTime = p.endTime.Sub(p.startTime).Seconds()
|
||||
|
||||
log.Infof("%s exec cost %.3fs err(%v)", p.jobEntry.detail.Name, p.jobEntry.detail.LastCostTime, err)
|
||||
return p.err
|
||||
}
|
||||
|
||||
type JobEntry struct {
|
||||
job *crontab.Job
|
||||
detail models.CrontabJob
|
||||
processNum int32
|
||||
processes map[uint32]*process
|
||||
pc int32
|
||||
wg util.WaitGroupWrapper
|
||||
logContent []byte
|
||||
jd *Jiacrontabd
|
||||
IDChan chan uint32
|
||||
IDGenerator uint32
|
||||
mux sync.RWMutex
|
||||
once bool // 只执行一次
|
||||
stop int32 // job stop status
|
||||
uniqueID string
|
||||
}
|
||||
|
||||
func newJobEntry(job *crontab.Job, jd *Jiacrontabd) *JobEntry {
|
||||
return &JobEntry{
|
||||
uniqueID: util.UUID(),
|
||||
job: job,
|
||||
IDChan: make(chan uint32, 10000),
|
||||
processes: make(map[uint32]*process),
|
||||
jd: jd,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobEntry) getLogPath() string {
|
||||
return filepath.Join(j.jd.getOpts().LogPath, "crontab_task", time.Now().Format("2006/01/02"), fmt.Sprintf("%d.log", j.job.ID))
|
||||
}
|
||||
|
||||
func (j *JobEntry) getLogDir() string {
|
||||
return filepath.Join(j.jd.getOpts().LogPath, "crontab_task")
|
||||
}
|
||||
|
||||
func (j *JobEntry) setOnce(v bool) {
|
||||
j.once = v
|
||||
}
|
||||
|
||||
func (j *JobEntry) takeID() uint32 {
|
||||
for {
|
||||
select {
|
||||
case id := <-j.IDChan:
|
||||
return id
|
||||
default:
|
||||
id := atomic.AddUint32(&j.IDGenerator, 1)
|
||||
if id != 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobEntry) putID(id uint32) {
|
||||
select {
|
||||
case j.IDChan <- id:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobEntry) writeLog() {
|
||||
writeFile(j.getLogPath(), &j.logContent)
|
||||
}
|
||||
|
||||
func (j *JobEntry) handleDepError(startTime time.Time, p *process) {
|
||||
cfg := j.jd.getOpts()
|
||||
err := fmt.Errorf("%s %s exec depend job err(%v)", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, p.err)
|
||||
endTime := time.Now()
|
||||
reply := true
|
||||
|
||||
j.logContent = append(j.logContent, []byte(err.Error()+"\n")...)
|
||||
j.detail.LastExitStatus = exitDependError
|
||||
j.writeLog()
|
||||
|
||||
if j.detail.ErrorMailNotify && len(j.detail.MailTo) != 0 {
|
||||
if err := j.jd.rpcCallCtx(context.TODO(), "Srv.SendMail", proto.SendMail{
|
||||
MailTo: j.detail.MailTo,
|
||||
Subject: cfg.BoardcastAddr + "提醒脚本依赖异常退出",
|
||||
Content: fmt.Sprintf(
|
||||
"任务名:%s<br/>创建者:%s<br/>开始时间:%s<br/>耗时:%.4f<br/>异常:%s",
|
||||
j.detail.Name, j.detail.CreatedUsername, startTime.Format(proto.DefaultTimeLayout), endTime.Sub(startTime).Seconds(), err),
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.SendMail error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// 钉钉webhook通知
|
||||
if j.detail.ErrorDingdingNotify && len(j.detail.DingdingTo) != 0 {
|
||||
nodeAddr := cfg.BoardcastAddr
|
||||
title := nodeAddr + "告警:脚本依赖异常退出"
|
||||
notifyContent := fmt.Sprintf("> ###### 来自jiacrontabd: %s 的依赖异常退出报警:\n> ##### 任务id:%d\n> ##### 任务名称:%s\n> ##### timeout:%d\n> ##### 尝试次数:%d\n> ##### 异常:%s\n> ##### 报警时间:%s", nodeAddr, int(j.detail.ID), j.detail.Name, int64(j.detail.Timeout), p.retryNum, err, time.Now().Format("2006-01-02 15:04:05"))
|
||||
notifyBody := fmt.Sprintf(
|
||||
`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "%s",
|
||||
"text": "%s"
|
||||
}
|
||||
}`, title, notifyContent)
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.ApiPost", proto.ApiPost{
|
||||
Urls: j.detail.DingdingTo,
|
||||
Data: notifyBody,
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.ApiPost error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobEntry) handleNotify(p *process) {
|
||||
|
||||
var (
|
||||
err error
|
||||
reply bool
|
||||
cfg = j.jd.getOpts()
|
||||
)
|
||||
|
||||
if p.err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if j.detail.ErrorMailNotify {
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.SendMail", proto.SendMail{
|
||||
MailTo: j.detail.MailTo,
|
||||
Subject: cfg.BoardcastAddr + "提醒脚本异常退出",
|
||||
Content: fmt.Sprintf(
|
||||
"任务名:%s<br/>创建者:%s<br/>开始时间:%s<br/>异常:%s<br/>重试次数:%d",
|
||||
j.detail.Name, j.detail.CreatedUsername,
|
||||
p.startTime.Format(proto.DefaultTimeLayout), p.err.Error(), p.retryNum),
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.SendMail error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if j.detail.ErrorAPINotify {
|
||||
postData, err := json.Marshal(proto.CrontabApiNotifyBody{
|
||||
NodeAddr: cfg.BoardcastAddr,
|
||||
JobName: j.detail.Name,
|
||||
JobID: int(j.detail.ID),
|
||||
CreateUsername: j.detail.CreatedUsername,
|
||||
CreatedAt: j.detail.CreatedAt,
|
||||
Timeout: int64(j.detail.Timeout),
|
||||
Type: "error",
|
||||
RetryNum: p.retryNum,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("json.Marshal error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.ApiPost", proto.ApiPost{
|
||||
Urls: j.detail.APITo,
|
||||
Data: string(postData),
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.ApiPost error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 钉钉webhook通知
|
||||
if j.detail.ErrorDingdingNotify {
|
||||
nodeAddr := cfg.BoardcastAddr
|
||||
title := nodeAddr + "告警:脚本异常退出"
|
||||
notifyContent := fmt.Sprintf("> ###### 来自jiacrontabd: %s 的脚本异常退出报警:\n> ##### 任务id:%d\n> ##### 任务名称:%s\n> ##### timeout:%d\n> ##### 尝试次数:%d\n> ##### 报警时间:%s", nodeAddr, int(j.detail.ID), j.detail.Name, int64(j.detail.Timeout), p.retryNum, time.Now().Format("2006-01-02 15:04:05"))
|
||||
notifyBody := fmt.Sprintf(
|
||||
`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "%s",
|
||||
"text": "%s"
|
||||
}
|
||||
}`, title, notifyContent)
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.ApiPost", proto.ApiPost{
|
||||
Urls: j.detail.DingdingTo,
|
||||
Data: notifyBody,
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.ApiPost error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobEntry) timeoutTrigger(p *process) {
|
||||
|
||||
var (
|
||||
err error
|
||||
reply bool
|
||||
cfg = j.jd.getOpts()
|
||||
)
|
||||
|
||||
for _, e := range j.detail.TimeoutTrigger {
|
||||
switch e {
|
||||
case proto.TimeoutTrigger_CallApi:
|
||||
j.detail.LastExitStatus = exitTimeout
|
||||
postData, err := json.Marshal(proto.CrontabApiNotifyBody{
|
||||
NodeAddr: cfg.BoardcastAddr,
|
||||
JobName: j.detail.Name,
|
||||
JobID: int(j.detail.ID),
|
||||
CreateUsername: j.detail.CreatedUsername,
|
||||
CreatedAt: j.detail.CreatedAt,
|
||||
Timeout: int64(j.detail.Timeout),
|
||||
Type: "timeout",
|
||||
RetryNum: p.retryNum,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("json.Marshal error:", err)
|
||||
}
|
||||
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.ApiPost err:", proto.ApiPost{
|
||||
Urls: j.detail.APITo,
|
||||
Data: string(postData),
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.ApiPost err:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
|
||||
case proto.TimeoutTrigger_SendEmail:
|
||||
j.detail.LastExitStatus = exitTimeout
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.SendMail", proto.SendMail{
|
||||
MailTo: j.detail.MailTo,
|
||||
Subject: cfg.BoardcastAddr + "提醒脚本执行超时",
|
||||
Content: fmt.Sprintf(
|
||||
"任务名:%s<br/>创建者:%v<br/>开始时间:%s<br/>超时:%ds<br/>重试次数:%d",
|
||||
j.detail.Name, j.detail.CreatedUsername, p.startTime.Format(proto.DefaultTimeLayout),
|
||||
j.detail.Timeout, p.retryNum),
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.SendMail error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
case proto.TimeoutTrigger_Kill:
|
||||
j.detail.LastExitStatus = exitTimeout
|
||||
p.cancel()
|
||||
case proto.TimeoutTrigger_DingdingWebhook:
|
||||
j.detail.LastExitStatus = exitTimeout
|
||||
|
||||
nodeAddr := cfg.BoardcastAddr
|
||||
title := nodeAddr + "告警:脚本执行超时"
|
||||
notifyContent := fmt.Sprintf("> ###### 来自jiacrontabd: %s 的脚本执行超时报警:\n> ##### 任务id:%d\n> ##### 任务名称:%s\n> ##### timeout:%d\n> ##### 尝试次数:%d\n> ##### 报警时间:%s", nodeAddr, int(j.detail.ID), j.detail.Name, int64(j.detail.Timeout), p.retryNum, time.Now().Format("2006-01-02 15:04:05"))
|
||||
notifyBody := fmt.Sprintf(
|
||||
`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "%s",
|
||||
"text": "%s"
|
||||
}
|
||||
}`, title, notifyContent)
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.ApiPost", proto.ApiPost{
|
||||
Urls: j.detail.DingdingTo,
|
||||
Data: notifyBody,
|
||||
}, &reply); err != nil {
|
||||
log.Error("Srv.ApiPost error:", err, "server addr:", cfg.AdminAddr)
|
||||
}
|
||||
default:
|
||||
log.Error("invalid timeoutTrigger", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLog return log data
|
||||
func (j *JobEntry) GetLog() []byte {
|
||||
return j.logContent
|
||||
}
|
||||
|
||||
func (j *JobEntry) exec() {
|
||||
|
||||
if atomic.LoadInt32(&j.stop) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
exec := func() {
|
||||
var err error
|
||||
now := time.Now()
|
||||
if j.once {
|
||||
err = models.DB().Take(&j.detail, "id=?", j.job.ID).Error
|
||||
atomic.StoreInt32(&j.processNum, int32(j.detail.ProcessNum))
|
||||
} else {
|
||||
err = models.DB().Take(&j.detail, "id=? and status in(?)",
|
||||
j.job.ID, []models.JobStatus{models.StatusJobTiming, models.StatusJobRunning}).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
j.jd.deleteJob(j.detail.ID)
|
||||
log.Warnf("jobID:%d JobEntry.exec:%v", j.detail.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !j.once {
|
||||
// 忽略一秒内重复执行的job
|
||||
if j.detail.LastExecTime.Truncate(time.Second).Equal(now.Truncate(time.Second)) {
|
||||
log.Infof("ignore repeat job %s", j.detail.Name)
|
||||
return
|
||||
}
|
||||
// 数据库中记录的执行时刻等于计时器中的时刻和现在的时刻才允许执行
|
||||
execTime := j.detail.NextExecTime.Truncate(time.Second)
|
||||
if !(execTime.Equal(j.job.GetNextExecTime().Truncate(time.Second)) && execTime.Equal(now.Truncate(time.Second))) {
|
||||
log.Errorf("%s(%d) JobEntry.exec time error(%s not equal %s)",
|
||||
j.detail.Name, j.detail.ID, execTime, now)
|
||||
j.jd.addJob(&crontab.Job{
|
||||
ID: j.detail.ID,
|
||||
Second: j.detail.TimeArgs.Second,
|
||||
Minute: j.detail.TimeArgs.Minute,
|
||||
Hour: j.detail.TimeArgs.Hour,
|
||||
Day: j.detail.TimeArgs.Day,
|
||||
Month: j.detail.TimeArgs.Month,
|
||||
Weekday: j.detail.TimeArgs.Weekday,
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
j.jd.addJob(j.job, true)
|
||||
}
|
||||
|
||||
if atomic.LoadInt32(&j.processNum) >= int32(j.detail.MaxConcurrent) && j.detail.MaxConcurrent != 0 {
|
||||
j.logContent = []byte("不得超过job最大并发数量\n")
|
||||
return
|
||||
}
|
||||
|
||||
if atomic.LoadInt32(&j.processNum) == 0 {
|
||||
j.logContent = nil
|
||||
}
|
||||
|
||||
atomic.AddInt32(&j.processNum, 1)
|
||||
|
||||
id := j.takeID()
|
||||
startTime := time.Now()
|
||||
var endTime time.Time
|
||||
defer func() {
|
||||
endTime = time.Now()
|
||||
atomic.AddInt32(&j.processNum, -1)
|
||||
j.updateJob(models.StatusJobTiming, startTime, endTime, err)
|
||||
}()
|
||||
|
||||
j.updateJob(models.StatusJobRunning, startTime, endTime, err)
|
||||
|
||||
p := newProcess(id, j)
|
||||
|
||||
j.mux.Lock()
|
||||
j.processes[id] = p
|
||||
j.mux.Unlock()
|
||||
|
||||
defer func() {
|
||||
j.mux.Lock()
|
||||
delete(j.processes, id)
|
||||
j.mux.Unlock()
|
||||
j.putID(id)
|
||||
}()
|
||||
|
||||
for i := 0; i <= j.detail.RetryNum; i++ {
|
||||
|
||||
if atomic.LoadInt32(&j.stop) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("jobID:", j.detail.ID, "retryNum:", i)
|
||||
|
||||
p.retryNum = i
|
||||
|
||||
// 执行脚本
|
||||
if err = p.exec(); err == nil || j.once {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if j.once {
|
||||
exec()
|
||||
return
|
||||
}
|
||||
|
||||
j.wg.Wrap(exec)
|
||||
}
|
||||
|
||||
func (j *JobEntry) updateJob(status models.JobStatus, startTime, endTime time.Time, err error) {
|
||||
data := map[string]interface{}{
|
||||
"status": status,
|
||||
"process_num": atomic.LoadInt32(&j.processNum),
|
||||
"last_exit_status": "",
|
||||
"failed": false,
|
||||
}
|
||||
|
||||
if endTime.After(startTime) {
|
||||
data["last_cost_time"] = endTime.Sub(startTime).Seconds()
|
||||
}
|
||||
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
data["last_exit_status"] = errMsg
|
||||
data["failed"] = true
|
||||
}
|
||||
|
||||
if j.once {
|
||||
delete(data, "status")
|
||||
delete(data, "last_exit_status")
|
||||
}
|
||||
|
||||
if status == models.StatusJobTiming {
|
||||
if err = j.jd.rpcCallCtx(context.TODO(), "Srv.PushJobLog", models.JobHistory{
|
||||
JobType: models.JobTypeCrontab,
|
||||
JobID: j.detail.ID,
|
||||
Addr: j.jd.getOpts().BoardcastAddr,
|
||||
JobName: j.detail.Name,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExitMsg: errMsg,
|
||||
}, nil); err != nil {
|
||||
log.Error("rpc call Srv.PushJobLog failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
models.DB().Model(&j.detail).Updates(data)
|
||||
}
|
||||
|
||||
func (j *JobEntry) kill() {
|
||||
j.exit()
|
||||
j.waitDone()
|
||||
}
|
||||
|
||||
func (j *JobEntry) waitDone() []byte {
|
||||
j.wg.Wait()
|
||||
atomic.StoreInt32(&j.stop, 0)
|
||||
return j.logContent
|
||||
}
|
||||
|
||||
func (j *JobEntry) exit() {
|
||||
atomic.StoreInt32(&j.stop, 1)
|
||||
j.mux.Lock()
|
||||
for _, v := range j.processes {
|
||||
v.cancel()
|
||||
}
|
||||
j.mux.Unlock()
|
||||
}
|
||||
571
jiacrontabd/srv.go
Normal file
571
jiacrontabd/srv.go
Normal file
@@ -0,0 +1,571 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"jiacrontab/models"
|
||||
"jiacrontab/pkg/crontab"
|
||||
"jiacrontab/pkg/file"
|
||||
"jiacrontab/pkg/finder"
|
||||
"jiacrontab/pkg/proto"
|
||||
"jiacrontab/pkg/util"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
type Srv struct {
|
||||
jd *Jiacrontabd
|
||||
}
|
||||
|
||||
func newSrv(jd *Jiacrontabd) *Srv {
|
||||
return &Srv{
|
||||
jd: jd,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Srv) Ping(args proto.EmptyArgs, reply *proto.EmptyReply) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Srv) SystemInfo(args proto.EmptyArgs, reply *map[string]interface{}) error {
|
||||
*reply = util.SystemInfo(s.jd.startTime)
|
||||
(*reply)["job日志文件大小"] = file.FileSize(file.DirSize(s.jd.getOpts().LogPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Srv) CleanLogFiles(args proto.CleanNodeLog, reply *proto.CleanNodeLogRet) error {
|
||||
dir := s.jd.getOpts().LogPath
|
||||
|
||||
var t time.Time
|
||||
if args.Unit == "month" {
|
||||
t = time.Now().AddDate(0, -args.Offset, 0)
|
||||
} else if args.Unit == "day" {
|
||||
t = time.Now().AddDate(0, 0, -args.Offset)
|
||||
}
|
||||
|
||||
total, size, err := file.Remove(dir, t)
|
||||
reply.Total = total
|
||||
reply.Size = file.FileSize(size)
|
||||
return err
|
||||
}
|
||||
|
||||
type CrontabJob struct {
|
||||
jd *Jiacrontabd
|
||||
}
|
||||
|
||||
func newCrontabJobSrv(jd *Jiacrontabd) *CrontabJob {
|
||||
return &CrontabJob{
|
||||
jd: jd,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CrontabJob) List(args proto.QueryJobArgs, reply *proto.QueryCrontabJobRet) error {
|
||||
model := models.DB().Model(&models.CrontabJob{})
|
||||
if args.SearchTxt != "" {
|
||||
txt := "%" + args.SearchTxt + "%"
|
||||
model = model.Where("(name like ? or command like ? or code like ?)", txt, txt, txt)
|
||||
}
|
||||
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
} else if args.Root {
|
||||
model = model.Where("group_id=?", args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id=? and group_id=?", args.UserID, args.GroupID)
|
||||
}
|
||||
err := model.Count(&reply.Total).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Page = args.Page
|
||||
reply.Pagesize = args.Pagesize
|
||||
|
||||
return model.Order(fmt.Sprintf("created_user_id=%d desc, id desc", args.UserID)).Offset((args.Page - 1) * args.Pagesize).Limit(args.Pagesize).Find(&reply.List).Error
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Audit(args proto.AuditJobArgs, reply *[]models.CrontabJob) error {
|
||||
model := models.DB().Model(&models.CrontabJob{})
|
||||
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?)", args.JobIDs)
|
||||
} else {
|
||||
model = model.Where("id in (?) and group_id=?", args.JobIDs, args.GroupID)
|
||||
}
|
||||
|
||||
defer model.Find(reply)
|
||||
return model.Where("status=?", models.StatusJobUnaudited).Update("status", models.StatusJobOk).Error
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Edit(args proto.EditCrontabJobArgs, reply *models.CrontabJob) error {
|
||||
|
||||
var (
|
||||
model = models.DB()
|
||||
)
|
||||
|
||||
if args.Job.MaxConcurrent == 0 {
|
||||
args.Job.MaxConcurrent = 1
|
||||
}
|
||||
|
||||
if args.Job.ID == 0 {
|
||||
model = models.DB().Save(&args.Job)
|
||||
} else {
|
||||
// we should kill the job
|
||||
j.jd.killTask(args.Job.ID)
|
||||
|
||||
j.jd.mux.Lock()
|
||||
delete(j.jd.jobs, args.Job.ID)
|
||||
j.jd.mux.Unlock()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id=?", args.Job.ID)
|
||||
} else if args.Root {
|
||||
model = model.Where("id=? and group_id=?", args.Job.ID, args.Job.GroupID)
|
||||
} else {
|
||||
model = model.Where("id=? and created_user_id=? and group_id=?", args.Job.ID, args.Job.CreatedUserID, args.Job.GroupID)
|
||||
}
|
||||
args.Job.NextExecTime = time.Time{}
|
||||
model = model.Omit(
|
||||
"updated_at", "created_at", "deleted_at",
|
||||
"created_user_id", "created_username",
|
||||
"last_cost_time", "last_exec_time", "group_id",
|
||||
"last_exit_status", "process_num",
|
||||
).Save(&args.Job)
|
||||
}
|
||||
*reply = args.Job
|
||||
return model.Error
|
||||
}
|
||||
func (j *CrontabJob) Get(args proto.GetJobArgs, reply *models.CrontabJob) error {
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id=?", args.JobID)
|
||||
} else if args.Root {
|
||||
model = model.Where("id=? and group_id=?", args.JobID, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("id=? and created_user_id=? and group_id=?", args.JobID, args.UserID, args.GroupID)
|
||||
}
|
||||
return model.Find(reply).Error
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Start(args proto.ActionJobsArgs, jobs *[]models.CrontabJob) error {
|
||||
|
||||
model := models.DB()
|
||||
|
||||
if len(args.JobIDs) == 0 {
|
||||
return errors.New("empty ids")
|
||||
}
|
||||
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?) and status in (?)",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobOk, models.StatusJobStop})
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and status in (?) and group_id=?",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobOk, models.StatusJobStop}, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in (?) and status in (?) and group_id=?",
|
||||
args.UserID, args.JobIDs, []models.JobStatus{models.StatusJobOk, models.StatusJobStop}, args.GroupID)
|
||||
}
|
||||
|
||||
ret := model.Find(jobs)
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
for _, v := range *jobs {
|
||||
err := j.jd.addJob(&crontab.Job{
|
||||
ID: v.ID,
|
||||
Second: v.TimeArgs.Second,
|
||||
Minute: v.TimeArgs.Minute,
|
||||
Hour: v.TimeArgs.Hour,
|
||||
Day: v.TimeArgs.Day,
|
||||
Month: v.TimeArgs.Month,
|
||||
Weekday: v.TimeArgs.Weekday,
|
||||
}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Stop(args proto.ActionJobsArgs, jobs *[]models.CrontabJob) error {
|
||||
model := models.DB().Model(&models.CrontabJob{})
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?) and status in (?)", args.JobIDs, []models.JobStatus{models.StatusJobTiming, models.StatusJobRunning})
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and status in (?) and group_id=?",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobTiming, models.StatusJobRunning}, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in (?) and status in (?) and group_id=?",
|
||||
args.UserID, args.JobIDs, []models.JobStatus{models.StatusJobTiming, models.StatusJobRunning}, args.GroupID)
|
||||
}
|
||||
|
||||
for _, jobID := range args.JobIDs {
|
||||
j.jd.killTask(jobID)
|
||||
}
|
||||
|
||||
return model.Updates(map[string]interface{}{
|
||||
"status": models.StatusJobStop,
|
||||
"next_exec_time": time.Time{},
|
||||
}).Find(jobs).Error
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Delete(args proto.ActionJobsArgs, job *[]models.CrontabJob) error {
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?)", args.JobIDs)
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and group_id=?", args.JobIDs, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in (?) and group_id=?",
|
||||
args.UserID, args.JobIDs, args.GroupID)
|
||||
}
|
||||
return model.Find(job).Delete(&models.CrontabJob{}).Error
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Kill(args proto.ActionJobsArgs, job *[]models.CrontabJob) error {
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?)", args.JobIDs)
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and group_id=?", args.JobIDs, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in (?) and group_id=?",
|
||||
args.UserID, args.JobIDs, args.GroupID)
|
||||
}
|
||||
|
||||
err := model.Take(job).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, jobID := range args.JobIDs {
|
||||
j.jd.killTask(jobID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Execs(args proto.ActionJobsArgs, reply *[]models.CrontabJob) error {
|
||||
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?)", args.JobIDs)
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and group_id=?", args.JobIDs, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id=? and id in (?) and group_id=?", args.UserID, args.JobIDs, args.GroupID)
|
||||
}
|
||||
|
||||
var jobs []models.CrontabJob
|
||||
if err := model.Find(&jobs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range jobs {
|
||||
*reply = append(*reply, v)
|
||||
go func(v models.CrontabJob) {
|
||||
ins := newJobEntry(&crontab.Job{
|
||||
ID: v.ID,
|
||||
Value: v,
|
||||
}, j.jd)
|
||||
ins.setOnce(true)
|
||||
j.jd.addTmpJob(ins)
|
||||
defer j.jd.removeTmpJob(ins)
|
||||
ins.once = true
|
||||
ins.exec()
|
||||
}(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Exec(args proto.GetJobArgs, reply *proto.ExecCrontabJobReply) error {
|
||||
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id=?", args.JobID)
|
||||
} else if args.Root {
|
||||
model = model.Where("id=? and group_id=?", args.JobID, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id=? and group_id=?", args.UserID, args.JobID, args.GroupID)
|
||||
}
|
||||
|
||||
err := model.Take(&reply.Job).Error
|
||||
|
||||
if err == nil {
|
||||
ins := newJobEntry(&crontab.Job{
|
||||
ID: reply.Job.ID,
|
||||
Value: reply.Job,
|
||||
}, j.jd)
|
||||
ins.setOnce(true)
|
||||
j.jd.addTmpJob(ins)
|
||||
defer j.jd.removeTmpJob(ins)
|
||||
ins.once = true
|
||||
ins.exec()
|
||||
reply.Content = ins.GetLog()
|
||||
} else {
|
||||
reply.Content = []byte(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Log(args proto.SearchLog, reply *proto.SearchLogResult) error {
|
||||
fd := finder.NewFinder(func(info os.FileInfo) bool {
|
||||
basename := filepath.Base(info.Name())
|
||||
arr := strings.Split(basename, ".")
|
||||
if len(arr) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if arr[1] == "log" && arr[0] == fmt.Sprint(args.JobID) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if args.Date == "" {
|
||||
args.Date = time.Now().Format("2006/01/02")
|
||||
}
|
||||
if args.IsTail {
|
||||
fd.SetTail(true)
|
||||
}
|
||||
|
||||
rootpath := filepath.Join(j.jd.getOpts().LogPath, "crontab_task", args.Date)
|
||||
err := fd.Search(rootpath, args.Pattern, &reply.Content, args.Offset, args.Pagesize)
|
||||
reply.Offset = fd.Offset()
|
||||
reply.FileSize = fd.FileSize()
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// SetDependDone 依赖执行完毕时设置相关状态
|
||||
func (j *CrontabJob) SetDependDone(args proto.DepJob, reply *bool) error {
|
||||
*reply = j.jd.SetDependDone(&depEntry{
|
||||
jobID: args.JobID,
|
||||
processID: args.ProcessID,
|
||||
jobUniqueID: args.JobUniqueID,
|
||||
id: args.ID,
|
||||
dest: args.Dest,
|
||||
from: args.From,
|
||||
done: true,
|
||||
logContent: args.LogContent,
|
||||
err: args.Err,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecDepend 执行依赖
|
||||
func (j *CrontabJob) ExecDepend(args proto.DepJob, reply *bool) error {
|
||||
j.jd.dep.add(&depEntry{
|
||||
jobUniqueID: args.JobUniqueID,
|
||||
processID: args.ProcessID,
|
||||
jobID: args.JobID,
|
||||
id: args.ID,
|
||||
dest: args.Dest,
|
||||
from: args.From,
|
||||
name: args.Name,
|
||||
commands: args.Commands,
|
||||
})
|
||||
*reply = true
|
||||
log.Infof("job %s %v add to execution queue ", args.Name, args.Commands)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CrontabJob) Ping(args *proto.EmptyArgs, reply *proto.EmptyReply) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type DaemonJob struct {
|
||||
jd *Jiacrontabd
|
||||
}
|
||||
|
||||
func newDaemonJobSrv(jd *Jiacrontabd) *DaemonJob {
|
||||
return &DaemonJob{
|
||||
jd: jd,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *DaemonJob) List(args proto.QueryJobArgs, reply *proto.QueryDaemonJobRet) error {
|
||||
|
||||
model := models.DB().Model(&models.DaemonJob{})
|
||||
if args.SearchTxt != "" {
|
||||
txt := "%" + args.SearchTxt + "%"
|
||||
model = model.Where("(name like ? or command like ? or code like ?)",
|
||||
txt, txt, txt)
|
||||
}
|
||||
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
} else if args.Root {
|
||||
model = model.Where("group_id=?", args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id=? and group_id=?", args.UserID, args.GroupID)
|
||||
}
|
||||
|
||||
err := model.Count(&reply.Total).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Page = args.Page
|
||||
reply.Pagesize = args.Pagesize
|
||||
|
||||
return model.Order(fmt.Sprintf("created_user_id=%d desc, id desc", args.UserID)).Offset((args.Page - 1) * args.Pagesize).Limit(args.Pagesize).Find(&reply.List).Error
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Edit(args proto.EditDaemonJobArgs, job *models.DaemonJob) error {
|
||||
|
||||
model := models.DB()
|
||||
if args.Job.ID == 0 {
|
||||
model = models.DB().Create(&args.Job)
|
||||
} else {
|
||||
j.jd.daemon.lock.Lock()
|
||||
delete(j.jd.daemon.taskMap, args.Job.ID)
|
||||
j.jd.daemon.lock.Unlock()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id=?", args.Job.ID)
|
||||
} else if args.Root {
|
||||
model = model.Where("id=? and group_id=?", args.Job.ID, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("id=? and created_user_id=? and group_id=?", args.Job.ID, args.Job.CreatedUserID, args.GroupID)
|
||||
}
|
||||
model = model.Omit(
|
||||
"updated_at", "created_at", "deleted_at", "group_id",
|
||||
"created_user_id", "created_username", "start_at").Save(&args.Job)
|
||||
}
|
||||
|
||||
*job = args.Job
|
||||
return model.Error
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Start(args proto.ActionJobsArgs, jobs *[]models.DaemonJob) error {
|
||||
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?) and status in (?)",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobOk, models.StatusJobStop})
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and status in (?) and group_id=?",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobOk, models.StatusJobStop}, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in (?) and status in (?) and group_id=?",
|
||||
args.UserID, args.JobIDs, []models.JobStatus{models.StatusJobOk, models.StatusJobStop}, args.GroupID)
|
||||
}
|
||||
|
||||
ret := model.Find(&jobs)
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
for _, v := range *jobs {
|
||||
job := v
|
||||
j.jd.daemon.add(&daemonJob{
|
||||
job: &job,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Stop(args proto.ActionJobsArgs, jobs *[]models.DaemonJob) error {
|
||||
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?) and status in (?)",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobRunning, models.StatusJobTiming})
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and status in (?) and group_id=?",
|
||||
args.JobIDs, []models.JobStatus{models.StatusJobRunning, models.StatusJobTiming}, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in (?) and status in (?) and group_id=?",
|
||||
args.UserID, args.JobIDs, []models.JobStatus{models.StatusJobRunning, models.StatusJobTiming}, args.GroupID)
|
||||
}
|
||||
|
||||
if err := model.Find(jobs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
args.JobIDs = nil
|
||||
for _, job := range *jobs {
|
||||
args.JobIDs = append(args.JobIDs, job.ID)
|
||||
j.jd.daemon.PopJob(job.ID)
|
||||
}
|
||||
|
||||
return model.Model(&models.DaemonJob{}).Update("status", models.StatusJobStop).Error
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Delete(args proto.ActionJobsArgs, jobs *[]models.DaemonJob) error {
|
||||
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?)", args.JobIDs)
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and group_id=?",
|
||||
args.JobIDs, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("created_user_id = ? and id in(?) and group_id=?",
|
||||
args.UserID, args.JobIDs, args.GroupID)
|
||||
}
|
||||
|
||||
if err := model.Find(jobs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range *jobs {
|
||||
j.jd.daemon.PopJob(job.ID)
|
||||
}
|
||||
return model.Delete(&models.DaemonJob{}).Error
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Get(args proto.GetJobArgs, job *models.DaemonJob) error {
|
||||
model := models.DB()
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id=?", args.JobID)
|
||||
} else if args.Root {
|
||||
model = model.Where("id=? and group_id=?", args.JobID, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("id=? and group_id=? and created_user_id=?", args.JobID, args.GroupID, args.UserID)
|
||||
}
|
||||
return model.Take(job).Error
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Log(args proto.SearchLog, reply *proto.SearchLogResult) error {
|
||||
|
||||
fd := finder.NewFinder(func(info os.FileInfo) bool {
|
||||
basename := filepath.Base(info.Name())
|
||||
arr := strings.Split(basename, ".")
|
||||
if len(arr) != 2 {
|
||||
return false
|
||||
}
|
||||
if arr[1] == "log" && arr[0] == fmt.Sprint(args.JobID) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if args.Date == "" {
|
||||
args.Date = time.Now().Format("2006/01/02")
|
||||
}
|
||||
|
||||
if args.IsTail {
|
||||
fd.SetTail(true)
|
||||
}
|
||||
|
||||
rootpath := filepath.Join(j.jd.getOpts().LogPath, "daemon_job", args.Date)
|
||||
err := fd.Search(rootpath, args.Pattern, &reply.Content, args.Offset, args.Pagesize)
|
||||
reply.Offset = fd.Offset()
|
||||
reply.FileSize = fd.FileSize()
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (j *DaemonJob) Audit(args proto.AuditJobArgs, jobs *[]models.DaemonJob) error {
|
||||
model := models.DB().Model(&models.DaemonJob{})
|
||||
if args.GroupID == models.SuperGroup.ID {
|
||||
model = model.Where("id in (?)", args.JobIDs)
|
||||
} else if args.Root {
|
||||
model = model.Where("id in (?) and group_id=?", args.JobIDs, args.GroupID)
|
||||
} else {
|
||||
model = model.Where("id in (?) and group_id=? and created_user_id=?", args.JobIDs, args.GroupID, args.UserID)
|
||||
}
|
||||
return model.Where("status=?", models.StatusJobUnaudited).Find(jobs).Update("status", models.StatusJobOk).Error
|
||||
}
|
||||
83
jiacrontabd/util.go
Normal file
83
jiacrontabd/util.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package jiacrontabd
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/util"
|
||||
"os"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
"container/list"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeFile(fPath string, content *[]byte) {
|
||||
f, err := util.TryOpen(fPath, os.O_APPEND|os.O_CREATE|os.O_RDWR)
|
||||
if err != nil {
|
||||
log.Errorf("writeLog: %v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
f.Write(*content)
|
||||
}
|
||||
|
||||
func GetIntranetIpList() *list.List {
|
||||
ipList := list.New()
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
|
||||
if err != nil {
|
||||
return ipList
|
||||
}
|
||||
|
||||
for _, address := range addrs {
|
||||
// 检查ip地址判断是否回环地址
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ipList.PushBack(ipnet.IP.String())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ipList
|
||||
}
|
||||
|
||||
func isIpBelong(ip, cidr string) bool {
|
||||
ipAddr := strings.Split(ip, `.`)
|
||||
if len(ipAddr) < 4 {
|
||||
return false
|
||||
}
|
||||
if ip == cidr {
|
||||
return true
|
||||
}
|
||||
cidrArr := strings.Split(cidr, `/`)
|
||||
if len(cidrArr) < 2 {
|
||||
return false
|
||||
}
|
||||
var tmp = make([]string, 0)
|
||||
for key, value := range strings.Split(`255.255.255.0`, `.`) {
|
||||
iint, _ := strconv.Atoi(value)
|
||||
|
||||
iint2, _ := strconv.Atoi(ipAddr[key])
|
||||
|
||||
tmp = append(tmp, strconv.Itoa(iint&iint2))
|
||||
}
|
||||
return strings.Join(tmp, `.`) == cidrArr[0]
|
||||
}
|
||||
|
||||
func checkIpInWhiteList(whiteIpStr string) bool {
|
||||
myIps := GetIntranetIpList()
|
||||
whiteIpList := strings.Split(whiteIpStr, `,`)
|
||||
if len(whiteIpList) == 0 {
|
||||
return true
|
||||
}
|
||||
for item := myIps.Front(); nil != item; item = item.Next() {
|
||||
for i := range whiteIpList {
|
||||
isBelong := isIpBelong(item.Value.(string), whiteIpList[i])
|
||||
if isBelong {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
222
models/crontab.go
Normal file
222
models/crontab.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"jiacrontab/pkg/util"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JobStatus 任务状态
|
||||
type JobStatus int
|
||||
|
||||
const (
|
||||
// StatusJobUnaudited 未审核
|
||||
StatusJobUnaudited JobStatus = 0
|
||||
// StatusJobOk 等待调度
|
||||
StatusJobOk JobStatus = 1
|
||||
// StatusJobTiming 定时中
|
||||
StatusJobTiming JobStatus = 2
|
||||
// StatusJobRunning 执行中
|
||||
StatusJobRunning JobStatus = 3
|
||||
// StatusJobStop 已停止
|
||||
StatusJobStop JobStatus = 4
|
||||
)
|
||||
|
||||
type CrontabJob struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"index;not null"`
|
||||
GroupID uint `json:"groupID" grom:"index"`
|
||||
Command StringSlice `json:"command" gorm:"type:varchar(1000)"`
|
||||
Code string `json:"code" gorm:"type:TEXT"`
|
||||
DependJobs DependJobs `json:"dependJobs" gorm:"type:TEXT"`
|
||||
LastCostTime float64 `json:"lastCostTime"`
|
||||
LastExecTime time.Time `json:"lastExecTime"`
|
||||
NextExecTime time.Time `json:"nextExecTime"`
|
||||
Failed bool `json:"failed"`
|
||||
LastExitStatus string `json:"lastExitStatus" grom:"index"`
|
||||
CreatedUserID uint `json:"createdUserId"`
|
||||
CreatedUsername string `json:"createdUsername"`
|
||||
UpdatedUserID uint `json:"updatedUserID"`
|
||||
UpdatedUsername string `json:"updatedUsername"`
|
||||
WorkUser string `json:"workUser"`
|
||||
WorkIp StringSlice `json:"workIp" gorm:"type:varchar(1000)"`
|
||||
WorkEnv StringSlice `json:"workEnv" gorm:"type:varchar(1000)"`
|
||||
WorkDir string `json:"workDir"`
|
||||
KillChildProcess bool `json:"killChildProcess"`
|
||||
Timeout int `json:"timeout"`
|
||||
ProcessNum int `json:"processNum"`
|
||||
ErrorMailNotify bool `json:"errorMailNotify"`
|
||||
ErrorAPINotify bool `json:"errorAPINotify"`
|
||||
ErrorDingdingNotify bool `json:"errorDingdingNotify"`
|
||||
RetryNum int `json:"retryNum"`
|
||||
Status JobStatus `json:"status"`
|
||||
IsSync bool `json:"isSync"` // 脚本是否同步执行
|
||||
MailTo StringSlice `json:"mailTo" gorm:"type:varchar(1000)"`
|
||||
APITo StringSlice `json:"APITo" gorm:"type:varchar(1000)"`
|
||||
DingdingTo StringSlice `json:"DingdingTo" gorm:"type:varchar(1000)"`
|
||||
MaxConcurrent uint `json:"maxConcurrent"` // 脚本最大并发量
|
||||
TimeoutTrigger StringSlice `json:"timeoutTrigger" gorm:"type:varchar(20)"`
|
||||
TimeArgs TimeArgs `json:"timeArgs" gorm:"type:TEXT"`
|
||||
}
|
||||
|
||||
type StringSlice []string
|
||||
|
||||
func (s *StringSlice) Scan(v interface{}) error {
|
||||
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return json.Unmarshal([]byte(val), s)
|
||||
case []byte:
|
||||
return json.Unmarshal(val, s)
|
||||
default:
|
||||
return errors.New("not support")
|
||||
}
|
||||
}
|
||||
|
||||
func (s StringSlice) MarshalJSON() ([]byte, error) {
|
||||
if s == nil {
|
||||
s = make(StringSlice, 0)
|
||||
}
|
||||
return json.Marshal([]string(s))
|
||||
}
|
||||
|
||||
func (s StringSlice) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
s = make(StringSlice, 0)
|
||||
}
|
||||
bts, err := json.Marshal(s)
|
||||
return string(bts), err
|
||||
}
|
||||
|
||||
type DependJobs []DependJob
|
||||
|
||||
func (d *DependJobs) Scan(v interface{}) error {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return json.Unmarshal([]byte(val), d)
|
||||
case []byte:
|
||||
return json.Unmarshal(val, d)
|
||||
default:
|
||||
return errors.New("not support")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d DependJobs) Value() (driver.Value, error) {
|
||||
|
||||
if d == nil {
|
||||
d = make(DependJobs, 0)
|
||||
}
|
||||
|
||||
for k, _ := range d {
|
||||
d[k].ID = util.UUID()
|
||||
}
|
||||
|
||||
bts, err := json.Marshal(d)
|
||||
return string(bts), err
|
||||
}
|
||||
|
||||
func (d DependJobs) MarshalJSON() ([]byte, error) {
|
||||
if d == nil {
|
||||
d = make(DependJobs, 0)
|
||||
}
|
||||
type m DependJobs
|
||||
return json.Marshal(m(d))
|
||||
}
|
||||
|
||||
type TimeArgs struct {
|
||||
Weekday string `json:"weekday"`
|
||||
Month string `json:"month"`
|
||||
Day string `json:"day"`
|
||||
Hour string `json:"hour"`
|
||||
Minute string `json:"minute"`
|
||||
Second string `json:"second"`
|
||||
}
|
||||
|
||||
func (c *TimeArgs) Scan(v interface{}) error {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return json.Unmarshal([]byte(val), c)
|
||||
case []byte:
|
||||
return json.Unmarshal(val, c)
|
||||
default:
|
||||
return errors.New("not support")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c TimeArgs) Value() (driver.Value, error) {
|
||||
bts, err := json.Marshal(c)
|
||||
return string(bts), err
|
||||
}
|
||||
|
||||
type DependJob struct {
|
||||
Dest string `json:"dest"`
|
||||
From string `json:"from"`
|
||||
JobID uint `json:"jobID"`
|
||||
ID string `json:"id"`
|
||||
WorkUser string `json:"user"`
|
||||
WorkDir string `json:"workDir"`
|
||||
Command []string `json:"command"`
|
||||
Code string `json:"code"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
||||
|
||||
type PipeComamnds [][]string
|
||||
|
||||
func (p *PipeComamnds) Scan(v interface{}) error {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return json.Unmarshal([]byte(val), p)
|
||||
case []byte:
|
||||
return json.Unmarshal(val, p)
|
||||
default:
|
||||
return errors.New("not support")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p PipeComamnds) Value() (driver.Value, error) {
|
||||
if p == nil {
|
||||
p = make(PipeComamnds, 0)
|
||||
}
|
||||
bts, err := json.Marshal(p)
|
||||
return string(bts), err
|
||||
}
|
||||
|
||||
func (d PipeComamnds) MarshalJSON() ([]byte, error) {
|
||||
if d == nil {
|
||||
d = make(PipeComamnds, 0)
|
||||
}
|
||||
type m PipeComamnds
|
||||
return json.Marshal(m(d))
|
||||
}
|
||||
|
||||
type CrontabArgs struct {
|
||||
Weekday string
|
||||
Month string
|
||||
Day string
|
||||
Hour string
|
||||
Minute string
|
||||
}
|
||||
|
||||
func (c *CrontabArgs) Scan(v interface{}) error {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return json.Unmarshal([]byte(val), c)
|
||||
case []byte:
|
||||
return json.Unmarshal(val, c)
|
||||
default:
|
||||
return errors.New("not support")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c CrontabArgs) Value() (driver.Value, error) {
|
||||
bts, err := json.Marshal(c)
|
||||
return string(bts), err
|
||||
}
|
||||
9
models/crontab_test.go
Normal file
9
models/crontab_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringSlice_Value(t *testing.T) {
|
||||
|
||||
}
|
||||
33
models/daemon.go
Normal file
33
models/daemon.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DaemonJob struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"index;not null"`
|
||||
GroupID uint `json:"groupID" grom:"index"`
|
||||
Command StringSlice `json:"command" gorm:"type:varchar(1000)"`
|
||||
Code string `json:"code" gorm:"type:TEXT"`
|
||||
ErrorMailNotify bool `json:"errorMailNotify"`
|
||||
ErrorAPINotify bool `json:"errorAPINotify"`
|
||||
ErrorDingdingNotify bool `json:"errorDingdingNotify"`
|
||||
Status JobStatus `json:"status"`
|
||||
MailTo StringSlice `json:"mailTo" gorm:"type:varchar(1000)"`
|
||||
APITo StringSlice `json:"APITo" gorm:"type:varchar(1000)"`
|
||||
DingdingTo StringSlice `json:"DingdingTo" gorm:"type:varchar(1000)"`
|
||||
FailRestart bool `json:"failRestart"`
|
||||
RetryNum int `json:"retryNum"`
|
||||
StartAt time.Time `json:"startAt"`
|
||||
WorkUser string `json:"workUser"`
|
||||
WorkIp StringSlice `json:"workIp" gorm:"type:varchar(1000)"`
|
||||
WorkEnv StringSlice `json:"workEnv" gorm:"type:varchar(1000)"`
|
||||
WorkDir string `json:"workDir"`
|
||||
CreatedUserID uint `json:"createdUserId"`
|
||||
CreatedUsername string `json:"createdUsername"`
|
||||
UpdatedUserID uint `json:"updatedUserID"`
|
||||
UpdatedUsername string `json:"updatedUsername"`
|
||||
}
|
||||
115
models/db.go
Normal file
115
models/db.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// D alias DB
|
||||
type D = gorm.DB
|
||||
|
||||
var (
|
||||
db *D
|
||||
debugMode bool
|
||||
)
|
||||
|
||||
func CreateDB(dialect string, dsn string) (err error) {
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
return createSqlite(dsn)
|
||||
case "mysql":
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
PrepareStmt: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
return
|
||||
case "postgres":
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
PrepareStmt: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
return fmt.Errorf("unknow database type %s", dialect)
|
||||
}
|
||||
|
||||
func createSqlite(dsn string) error {
|
||||
var err error
|
||||
if dsn == "" {
|
||||
return errors.New("sqlite:db file cannot empty")
|
||||
}
|
||||
|
||||
dbDir := filepath.Dir(filepath.Clean(dsn))
|
||||
err = os.MkdirAll(dbDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sqlite: makedir failed %s", err)
|
||||
}
|
||||
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err == nil {
|
||||
d, err := db.DB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d.SetMaxOpenConns(1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func DB() *D {
|
||||
if db == nil {
|
||||
panic("you must call CreateDb first")
|
||||
}
|
||||
if debugMode {
|
||||
return db.Debug()
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func Transactions(fn func(tx *gorm.DB) error) error {
|
||||
if fn == nil {
|
||||
return errors.New("fn is nil")
|
||||
}
|
||||
tx := DB().Begin()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
DB().Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if fn(tx) != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func InitModel(driverName string, dsn string, debug bool) error {
|
||||
if driverName == "" || dsn == "" {
|
||||
return errors.New("driverName and dsn cannot empty")
|
||||
}
|
||||
|
||||
if err := CreateDB(driverName, dsn); err != nil {
|
||||
return err
|
||||
}
|
||||
debugMode = debug
|
||||
AutoMigrate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func AutoMigrate() {
|
||||
if err := DB().AutoMigrate(&SysSetting{}, &Node{}, &Group{}, &User{}, &Event{}, &JobHistory{}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := DB().FirstOrCreate(&SuperGroup).Error; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
29
models/event.go
Normal file
29
models/event.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/iwannay/log"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EventSourceName string
|
||||
type EventSourceUsername string
|
||||
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
GroupID uint `json:"groupID" gorm:"index"`
|
||||
Username string `json:"username"`
|
||||
UserID uint `json:"userID" gorm:"index"`
|
||||
EventDesc string `json:"eventDesc"`
|
||||
TargetName string `json:"targetName"`
|
||||
SourceUsername string `json:"sourceUsername"`
|
||||
SourceName string `json:"sourceName" gorm:"index;size:500"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (e *Event) Pub() {
|
||||
err := DB().Model(e).Create(e).Error
|
||||
if err != nil {
|
||||
log.Error("Event.Pub", err)
|
||||
}
|
||||
}
|
||||
24
models/group.go
Normal file
24
models/group.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var SuperGroup Group
|
||||
|
||||
type Group struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"not null;uniqueIndex;size:500"`
|
||||
}
|
||||
|
||||
func (g *Group) Save() error {
|
||||
if g.ID == 0 {
|
||||
return DB().Create(g).Error
|
||||
}
|
||||
return DB().Save(g).Error
|
||||
}
|
||||
|
||||
func init() {
|
||||
SuperGroup.ID = 1
|
||||
SuperGroup.Name = "超级管理员"
|
||||
}
|
||||
33
models/history.go
Normal file
33
models/history.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
JobTypeCrontab JobType = 0
|
||||
JobTypeDaemon JobType = 1
|
||||
)
|
||||
|
||||
type JobType uint8
|
||||
|
||||
type JobHistory struct {
|
||||
gorm.Model
|
||||
JobType JobType `json:"jobType"` // 0:定时任务,1:常驻任务
|
||||
JobID uint `json:"jobID"`
|
||||
JobName string `json:"jobName"`
|
||||
Addr string `json:"addr" gorm:"index"`
|
||||
ExitMsg string `json:"exitMsg"`
|
||||
StartTime time.Time `json:"StartTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
func PushJobHistory(job *JobHistory) {
|
||||
err := DB().Create(job).Error
|
||||
if err != nil {
|
||||
log.Error("PushJobHistory failed:", err)
|
||||
}
|
||||
}
|
||||
96
models/node.go
Normal file
96
models/node.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
DaemonTaskNum uint `json:"daemonTaskNum"`
|
||||
Disabled bool `json:"disabled"` // 通信失败时Disabled会被设置为true
|
||||
CrontabTaskNum uint `json:"crontabTaskNum"`
|
||||
GroupID uint `json:"groupID" gorm:"not null;uniqueIndex:uni_group_addr" `
|
||||
CrontabJobAuditNum uint `json:"crontabJobAuditNum"`
|
||||
DaemonJobAuditNum uint `json:"daemonJobAuditNum"`
|
||||
CrontabJobFailNum uint `json:"crontabJobFailNum"`
|
||||
Addr string `json:"addr" gorm:"not null;uniqueIndex:uni_group_addr;size:100"`
|
||||
Group Group `json:"group"`
|
||||
}
|
||||
|
||||
func (n *Node) VerifyUserGroup(userID, groupID uint, addr string) bool {
|
||||
var user User
|
||||
|
||||
if groupID == SuperGroup.ID {
|
||||
return true
|
||||
}
|
||||
|
||||
if DB().Take(&user, "id=? and group_id=?", userID, groupID).Error != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return n.Exists(groupID, addr)
|
||||
}
|
||||
|
||||
func (n *Node) Delete(groupID uint, addr string) error {
|
||||
var ret *gorm.DB
|
||||
DB().Take(n, "group_id=? and addr=?", groupID, addr)
|
||||
if groupID == SuperGroup.ID {
|
||||
// 超级管理员分组采用软删除
|
||||
ret = DB().Delete(n, "group_id=? and addr=?", groupID, addr)
|
||||
} else {
|
||||
ret = DB().Unscoped().Delete(n, "group_id=? and addr=?", groupID, addr)
|
||||
}
|
||||
|
||||
if ret.Error != nil {
|
||||
return ret.Error
|
||||
}
|
||||
|
||||
if ret.RowsAffected == 0 {
|
||||
return errors.New("Delete failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) Rename(groupID uint, addr string) error {
|
||||
return DB().Model(n).Where("group_id=? and addr=?", groupID, addr).Updates(n).Error
|
||||
}
|
||||
|
||||
// GroupNode 为节点分组,复制groupID=1分组中node至目标分组
|
||||
func (n *Node) GroupNode(addr string, targetGroupID uint, targetNodeName, targetGroupName string) error {
|
||||
|
||||
// 新建分组
|
||||
if targetGroupID == 0 {
|
||||
group := &Group{
|
||||
Name: targetGroupName,
|
||||
}
|
||||
if err := DB().Save(group).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
targetGroupID = group.ID
|
||||
}
|
||||
|
||||
err := DB().Preload("Group").Where("group_id=? and addr=?", SuperGroup.ID, addr).Take(n).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if targetNodeName == "" {
|
||||
targetNodeName = n.Name
|
||||
}
|
||||
|
||||
return DB().Save(&Node{
|
||||
Addr: addr,
|
||||
GroupID: targetGroupID,
|
||||
Name: targetNodeName,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (n *Node) Exists(groupID uint, addr string) bool {
|
||||
if DB().Take(n, "group_id=? and addr=?", groupID, addr).Error != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
13
models/setting.go
Normal file
13
models/setting.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SysSetting struct {
|
||||
gorm.Model
|
||||
Class int `json:"class"` // 设置分类,1 Ldap配置
|
||||
Content json.RawMessage `json:"content" gorm:"column:content; type:json"` // 配置内容
|
||||
}
|
||||
116
models/user.go
Normal file
116
models/user.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"jiacrontab/pkg/util"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `json:"username" gorm:"not null;uniqueIndex;size:500"`
|
||||
Passwd string `json:"-"`
|
||||
Salt string `json:"-"`
|
||||
Avatar string `json:"avatar"`
|
||||
Version int64 `json:"version"`
|
||||
Gender string `json:"gender"`
|
||||
GroupID uint `json:"groupID" grom:"index"`
|
||||
Root bool `json:"root"`
|
||||
Mail string `json:"mail"`
|
||||
Group Group `json:"group"`
|
||||
}
|
||||
|
||||
func (u *User) getSalt() string {
|
||||
var (
|
||||
seed = "1234567890!@#$%^&*()ABCDEFGHIJK"
|
||||
salt [10]byte
|
||||
)
|
||||
for k := range salt {
|
||||
salt[k] = seed[util.RandIntn(len(seed))]
|
||||
}
|
||||
|
||||
return string(salt[0:10])
|
||||
}
|
||||
|
||||
// Verify 验证用户
|
||||
func (u *User) Verify(username, passwd string) bool {
|
||||
ret := DB().Take(u, "username=?", username)
|
||||
|
||||
if ret.Error != nil {
|
||||
log.Error("user.Verify:", ret.Error)
|
||||
return false
|
||||
}
|
||||
|
||||
bts := md5.Sum([]byte(fmt.Sprint(passwd, u.Salt)))
|
||||
return fmt.Sprintf("%x", bts) == u.Passwd
|
||||
}
|
||||
|
||||
// Verify 验证用户
|
||||
func (u *User) VerifyByUserId(id uint, passwd string) bool {
|
||||
ret := DB().Take(u, "id=?", id)
|
||||
|
||||
if ret.Error != nil {
|
||||
log.Error("user.Verify:", ret.Error)
|
||||
return false
|
||||
}
|
||||
|
||||
bts := md5.Sum([]byte(fmt.Sprint(passwd, u.Salt)))
|
||||
return fmt.Sprintf("%x", bts) == u.Passwd
|
||||
}
|
||||
|
||||
func (u *User) setPasswd() {
|
||||
if u.Passwd == "" {
|
||||
return
|
||||
}
|
||||
u.Salt = u.getSalt()
|
||||
bts := md5.Sum([]byte(fmt.Sprint(u.Passwd, u.Salt)))
|
||||
u.Passwd = fmt.Sprintf("%x", bts)
|
||||
}
|
||||
|
||||
func (u *User) Create() error {
|
||||
u.setPasswd()
|
||||
u.Version = time.Now().Unix()
|
||||
return DB().Create(u).Error
|
||||
}
|
||||
|
||||
func (u User) Update() error {
|
||||
u.setPasswd()
|
||||
u.Version = time.Now().Unix()
|
||||
if u.ID == 0 && u.Username != "" {
|
||||
return DB().Where("username=?", u.Username).Updates(u).Error
|
||||
}
|
||||
return DB().Model(&u).Updates(u).Error
|
||||
}
|
||||
|
||||
func (u *User) Delete() error {
|
||||
if err := DB().Take(u, "id=?", u.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return DB().Delete(u).Error
|
||||
}
|
||||
|
||||
func (u *User) SetGroup(group *Group) error {
|
||||
|
||||
if u.GroupID != 0 {
|
||||
if err := DB().Take(group, "id=?", u.GroupID).Error; err != nil {
|
||||
return fmt.Errorf("查询分组失败:%s", err)
|
||||
}
|
||||
}
|
||||
if u.ID == 1 {
|
||||
return errors.New("系统用户不允许修改")
|
||||
}
|
||||
|
||||
defer DB().Take(u, "id=?", u.ID)
|
||||
|
||||
return DB().Model(u).Where("id=?", u.ID).Updates(map[string]interface{}{
|
||||
"group_id": u.GroupID,
|
||||
"version": time.Now().Unix(),
|
||||
"root": u.Root,
|
||||
}).Error
|
||||
}
|
||||
278
pkg/base/stat.go
Normal file
278
pkg/base/stat.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
minuteTimeLayout = "200601021504"
|
||||
dateTimeLayout = "2006-01-02 15:04:05"
|
||||
defaultReserveMinutes = 60
|
||||
defaultCheckTimeMinutes = 10
|
||||
)
|
||||
|
||||
// Stat 应用内统计
|
||||
var Stat *stat
|
||||
|
||||
type (
|
||||
stat struct {
|
||||
ServerStartTime time.Time
|
||||
EnableDetailRequestData bool
|
||||
TotalRequestCount uint64
|
||||
|
||||
IntervalRequestData *Storage
|
||||
DetailRequestURLData *Storage
|
||||
TotalErrorCount uint64
|
||||
IntervalErrorData *Storage
|
||||
DetailErrorPageData *Storage
|
||||
DetailErrorData *Storage
|
||||
DetailHTTPCodeData *Storage
|
||||
|
||||
dataChanRequest chan *RequestInfo
|
||||
dataChanError chan *ErrorInfo
|
||||
dataChanHTTPCode chan *HttpCodeInfo
|
||||
TotalConcurrentCount int64
|
||||
|
||||
infoPool *pool
|
||||
}
|
||||
|
||||
pool struct {
|
||||
requestInfo sync.Pool
|
||||
errorInfo sync.Pool
|
||||
httpCodeInfo sync.Pool
|
||||
}
|
||||
|
||||
// RequestInfo 请求url信息
|
||||
RequestInfo struct {
|
||||
URL string
|
||||
Code int
|
||||
Num uint64
|
||||
}
|
||||
|
||||
ErrorInfo struct {
|
||||
URL string
|
||||
ErrMsg string
|
||||
Num uint64
|
||||
}
|
||||
|
||||
HttpCodeInfo struct {
|
||||
URL string
|
||||
Code int
|
||||
Num uint64
|
||||
}
|
||||
)
|
||||
|
||||
func (s *stat) QueryIntervalRequstData(key string) uint64 {
|
||||
val, _ := s.IntervalRequestData.GetUint64(key)
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *stat) QueryIntervalErrorData(key string) uint64 {
|
||||
val, _ := s.IntervalErrorData.GetUint64(key)
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *stat) AddRequestCount(page string, code int, num uint64) uint64 {
|
||||
|
||||
if !strings.HasPrefix(page, "/debug") {
|
||||
atomic.AddUint64(&s.TotalRequestCount, num)
|
||||
s.addRequestData(page, code, num)
|
||||
s.addHTTPCodeData(page, code, num)
|
||||
}
|
||||
atomic.AddInt64(&s.TotalConcurrentCount, -1)
|
||||
return atomic.LoadUint64(&s.TotalRequestCount)
|
||||
}
|
||||
|
||||
func (s *stat) AddConcurrentCount() {
|
||||
atomic.AddInt64(&s.TotalConcurrentCount, 1)
|
||||
}
|
||||
|
||||
func (s *stat) AddErrorCount(page string, err error, num uint64) uint64 {
|
||||
atomic.AddUint64(&s.TotalErrorCount, num)
|
||||
s.addErrorData(page, err, num)
|
||||
return atomic.LoadUint64(&s.TotalErrorCount)
|
||||
}
|
||||
|
||||
func (s *stat) addRequestData(page string, code int, num uint64) {
|
||||
info := s.infoPool.requestInfo.Get().(*RequestInfo)
|
||||
info.URL = page
|
||||
info.Code = code
|
||||
info.Num = num
|
||||
s.dataChanRequest <- info
|
||||
}
|
||||
|
||||
func (s *stat) addErrorData(page string, err error, num uint64) {
|
||||
info := s.infoPool.errorInfo.Get().(*ErrorInfo)
|
||||
info.URL = page
|
||||
info.ErrMsg = err.Error()
|
||||
info.Num = num
|
||||
s.dataChanError <- info
|
||||
}
|
||||
|
||||
func (s *stat) addHTTPCodeData(page string, code int, num uint64) {
|
||||
info := s.infoPool.httpCodeInfo.Get().(*HttpCodeInfo)
|
||||
info.URL = page
|
||||
info.Code = code
|
||||
info.Num = num
|
||||
s.dataChanHTTPCode <- info
|
||||
}
|
||||
|
||||
func (s *stat) handleInfo() {
|
||||
for {
|
||||
select {
|
||||
case info := <-s.dataChanRequest:
|
||||
{
|
||||
if s.EnableDetailRequestData {
|
||||
if info.Code != http.StatusNotFound {
|
||||
key := strings.ToLower(info.URL)
|
||||
val, _ := s.DetailRequestURLData.GetUint64(key)
|
||||
s.DetailRequestURLData.Store(key, val+info.Num)
|
||||
}
|
||||
}
|
||||
|
||||
key := time.Now().Format(minuteTimeLayout)
|
||||
val, _ := s.IntervalRequestData.GetUint64(key)
|
||||
s.IntervalRequestData.Store(key, val+info.Num)
|
||||
|
||||
s.infoPool.requestInfo.Put(info)
|
||||
}
|
||||
case info := <-s.dataChanError:
|
||||
{
|
||||
key := strings.ToLower(info.URL)
|
||||
val, _ := s.DetailErrorPageData.GetUint64(key)
|
||||
s.DetailErrorPageData.Store(key, val+info.Num)
|
||||
|
||||
key = info.ErrMsg
|
||||
|
||||
val, _ = s.DetailErrorData.GetUint64(key)
|
||||
|
||||
s.DetailErrorData.Store(key, val+info.Num)
|
||||
|
||||
key = time.Now().Format(minuteTimeLayout)
|
||||
val, _ = s.IntervalErrorData.GetUint64(key)
|
||||
s.IntervalErrorData.Store(key, val+info.Num)
|
||||
|
||||
s.infoPool.errorInfo.Put(info)
|
||||
|
||||
}
|
||||
|
||||
case info := <-s.dataChanHTTPCode:
|
||||
{
|
||||
key := strconv.Itoa(info.Code)
|
||||
val, _ := s.DetailHTTPCodeData.GetUint64(key)
|
||||
s.DetailHTTPCodeData.Store(key, val+info.Num)
|
||||
|
||||
s.infoPool.httpCodeInfo.Put(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stat) Collect() map[string]interface{} {
|
||||
var dataMap = make(map[string]interface{})
|
||||
dataMap["ServerStartTime"] = s.ServerStartTime.Format(dateTimeLayout)
|
||||
dataMap["TotalRequestCount"] = atomic.LoadUint64(&s.TotalRequestCount)
|
||||
dataMap["TotalConcurrentCount"] = atomic.LoadInt64(&s.TotalConcurrentCount)
|
||||
dataMap["TotalErrorCount"] = s.TotalErrorCount
|
||||
dataMap["IntervalRequestData"] = s.IntervalRequestData.All()
|
||||
dataMap["DetailRequestUrlData"] = s.DetailRequestURLData.All()
|
||||
dataMap["IntervalErrorData"] = s.IntervalErrorData.All()
|
||||
dataMap["DetailErrorPageData"] = s.DetailErrorPageData.All()
|
||||
dataMap["DetailErrorData"] = s.DetailErrorData.All()
|
||||
dataMap["DetailHttpCodeData"] = s.DetailHTTPCodeData.All()
|
||||
return dataMap
|
||||
}
|
||||
|
||||
func (s *stat) gc() {
|
||||
var needRemoveKey []string
|
||||
now, _ := time.Parse(minuteTimeLayout, time.Now().Format(minuteTimeLayout))
|
||||
|
||||
if s.IntervalRequestData.Len() > defaultReserveMinutes {
|
||||
s.IntervalRequestData.Range(func(key, val interface{}) bool {
|
||||
keyString := key.(string)
|
||||
if t, err := time.Parse(minuteTimeLayout, keyString); err != nil {
|
||||
needRemoveKey = append(needRemoveKey, keyString)
|
||||
} else {
|
||||
if now.Sub(t) > (defaultReserveMinutes * time.Minute) {
|
||||
needRemoveKey = append(needRemoveKey, keyString)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
for _, v := range needRemoveKey {
|
||||
s.IntervalRequestData.Delete(v)
|
||||
}
|
||||
|
||||
needRemoveKey = []string{}
|
||||
if s.IntervalErrorData.Len() > defaultReserveMinutes {
|
||||
s.IntervalErrorData.Range(func(key, val interface{}) bool {
|
||||
keyString := key.(string)
|
||||
if t, err := time.Parse(minuteTimeLayout, keyString); err != nil {
|
||||
needRemoveKey = append(needRemoveKey, keyString)
|
||||
} else {
|
||||
if now.Sub(t) > defaultReserveMinutes*time.Minute {
|
||||
needRemoveKey = append(needRemoveKey, keyString)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, v := range needRemoveKey {
|
||||
s.IntervalErrorData.Delete(v)
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Duration(defaultCheckTimeMinutes)*time.Minute, s.gc)
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
Stat = &stat{
|
||||
// 服务启动时间
|
||||
ServerStartTime: time.Now(),
|
||||
// 单位时间内请求数据 - 分钟为单位
|
||||
IntervalRequestData: NewStorage(),
|
||||
// 明细请求页面数据 - 以不带参数的访问url为key
|
||||
DetailRequestURLData: NewStorage(),
|
||||
// 单位时间内异常次数 - 按分钟为单位
|
||||
IntervalErrorData: NewStorage(),
|
||||
// 明细异常页面数据 - 以不带参数的访问url为key
|
||||
DetailErrorPageData: NewStorage(),
|
||||
// 单位时间内异常次数 - 按分钟为单位
|
||||
DetailErrorData: NewStorage(),
|
||||
// 明细Http状态码数据 - 以HttpCode为key,例如200、500等
|
||||
DetailHTTPCodeData: NewStorage(),
|
||||
dataChanRequest: make(chan *RequestInfo, 1000),
|
||||
dataChanError: make(chan *ErrorInfo, 1000),
|
||||
dataChanHTTPCode: make(chan *HttpCodeInfo, 1000),
|
||||
EnableDetailRequestData: true, //是否启用详细请求数据统计, 当url较多时,导致内存占用过大
|
||||
infoPool: &pool{
|
||||
requestInfo: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &RequestInfo{}
|
||||
},
|
||||
},
|
||||
errorInfo: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &ErrorInfo{}
|
||||
},
|
||||
},
|
||||
httpCodeInfo: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &HttpCodeInfo{}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
go Stat.handleInfo()
|
||||
go time.AfterFunc(time.Duration(defaultCheckTimeMinutes)*time.Minute, Stat.gc)
|
||||
}
|
||||
53
pkg/base/storage.go
Normal file
53
pkg/base/storage.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
Storage struct {
|
||||
sync.Map
|
||||
}
|
||||
)
|
||||
|
||||
func NewStorage() *Storage {
|
||||
return &Storage{}
|
||||
}
|
||||
|
||||
func (s *Storage) All() map[string]interface{} {
|
||||
data := make(map[string]interface{})
|
||||
s.Range(func(key, value interface{}) bool {
|
||||
|
||||
data[key.(string)] = value
|
||||
return true
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *Storage) Exists(key interface{}) bool {
|
||||
|
||||
_, ok := s.Load(key)
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *Storage) GetUint64(key interface{}) (uint64, bool) {
|
||||
val, ok := s.Load(key)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
ret, ok := val.(uint64)
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (s *Storage) Len() uint {
|
||||
var count uint
|
||||
s.Range(func(key, val interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
|
||||
return count
|
||||
|
||||
}
|
||||
88
pkg/crontab/crontab.go
Normal file
88
pkg/crontab/crontab.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package crontab
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"errors"
|
||||
"jiacrontab/pkg/pqueue"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task 任务
|
||||
type Task = pqueue.Item
|
||||
|
||||
type Crontab struct {
|
||||
pq pqueue.PriorityQueue
|
||||
mux sync.RWMutex
|
||||
ready chan *Task
|
||||
}
|
||||
|
||||
func New() *Crontab {
|
||||
return &Crontab{
|
||||
pq: pqueue.New(10000),
|
||||
ready: make(chan *Task, 10000),
|
||||
}
|
||||
}
|
||||
|
||||
// AddJob 添加未经处理的job
|
||||
func (c *Crontab) AddJob(j *Job) error {
|
||||
nt, err := j.NextExecutionTime(time.Now())
|
||||
if err != nil {
|
||||
return errors.New("Invalid execution time")
|
||||
}
|
||||
c.mux.Lock()
|
||||
heap.Push(&c.pq, &Task{
|
||||
Priority: nt.UnixNano(),
|
||||
Value: j,
|
||||
})
|
||||
c.mux.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddJob 添加延时任务
|
||||
func (c *Crontab) AddTask(t *Task) {
|
||||
c.mux.Lock()
|
||||
heap.Push(&c.pq, t)
|
||||
c.mux.Unlock()
|
||||
}
|
||||
|
||||
func (c *Crontab) Len() int {
|
||||
c.mux.RLock()
|
||||
len := len(c.pq)
|
||||
c.mux.RUnlock()
|
||||
return len
|
||||
}
|
||||
|
||||
func (c *Crontab) GetAllTask() []*Task {
|
||||
c.mux.Lock()
|
||||
list := c.pq
|
||||
c.mux.Unlock()
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *Crontab) Ready() <-chan *Task {
|
||||
return c.ready
|
||||
}
|
||||
|
||||
func (c *Crontab) QueueScanWorker() {
|
||||
refreshTicker := time.NewTicker(20 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-refreshTicker.C:
|
||||
if len(c.pq) == 0 {
|
||||
continue
|
||||
}
|
||||
start:
|
||||
c.mux.Lock()
|
||||
now := time.Now().UnixNano()
|
||||
job, _ := c.pq.PeekAndShift(now)
|
||||
c.mux.Unlock()
|
||||
if job == nil {
|
||||
continue
|
||||
}
|
||||
c.ready <- job
|
||||
goto start
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
57
pkg/crontab/crontab_test.go
Normal file
57
pkg/crontab/crontab_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package crontab
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_crontab_Ready(t *testing.T) {
|
||||
var timeLayout = "2006-01-02 15:04:05"
|
||||
c := New()
|
||||
now := time.Now().Add(6 * time.Second)
|
||||
c.AddTask(&Task{
|
||||
Value: "test1" + now.Format(timeLayout),
|
||||
Priority: now.UnixNano(),
|
||||
})
|
||||
|
||||
now = time.Now().Add(1 * time.Second)
|
||||
|
||||
c.AddTask(&Task{
|
||||
Value: "test2" + now.Format(timeLayout),
|
||||
Priority: now.UnixNano(),
|
||||
})
|
||||
now = time.Now().Add(3 * time.Second)
|
||||
|
||||
c.AddTask(&Task{
|
||||
Value: "test3" + now.Format(timeLayout),
|
||||
Priority: now.UnixNano(),
|
||||
})
|
||||
|
||||
now = time.Now().Add(4 * time.Second)
|
||||
c.AddTask(&Task{
|
||||
Value: "test4" + now.Format(timeLayout),
|
||||
Priority: now.UnixNano(),
|
||||
})
|
||||
|
||||
now = time.Now().Add(3 * time.Second)
|
||||
c.AddTask(&Task{
|
||||
Value: "test5" + now.Format(timeLayout),
|
||||
Priority: now.UnixNano(),
|
||||
})
|
||||
|
||||
bts, _ := json.MarshalIndent(c.GetAllTask(), "", "")
|
||||
fmt.Println(string(bts))
|
||||
|
||||
go c.QueueScanWorker()
|
||||
|
||||
go func() {
|
||||
for v := range c.Ready() {
|
||||
bts, _ := json.MarshalIndent(v, "", "")
|
||||
fmt.Println(string(bts))
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
213
pkg/crontab/job.go
Normal file
213
pkg/crontab/job.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// package crontab 实现定时调度
|
||||
// 借鉴https://github.com/robfig/cron
|
||||
// 部分实现添加注释
|
||||
// 向https://github.com/robfig/cron项目致敬
|
||||
package crontab
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"jiacrontab/pkg/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
starBit = 1 << 63
|
||||
)
|
||||
|
||||
type bounds struct {
|
||||
min, max uint
|
||||
names map[string]uint
|
||||
}
|
||||
|
||||
// The bounds for each field.
|
||||
var (
|
||||
seconds = bounds{0, 59, nil}
|
||||
minutes = bounds{0, 59, nil}
|
||||
hours = bounds{0, 23, nil}
|
||||
dom = bounds{1, 31, nil}
|
||||
months = bounds{1, 12, map[string]uint{
|
||||
"jan": 1,
|
||||
"feb": 2,
|
||||
"mar": 3,
|
||||
"apr": 4,
|
||||
"may": 5,
|
||||
"jun": 6,
|
||||
"jul": 7,
|
||||
"aug": 8,
|
||||
"sep": 9,
|
||||
"oct": 10,
|
||||
"nov": 11,
|
||||
"dec": 12,
|
||||
}}
|
||||
dow = bounds{0, 6, map[string]uint{
|
||||
"sun": 0,
|
||||
"mon": 1,
|
||||
"tue": 2,
|
||||
"wed": 3,
|
||||
"thu": 4,
|
||||
"fri": 5,
|
||||
"sat": 6,
|
||||
}}
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Second string
|
||||
Minute string
|
||||
Hour string
|
||||
Day string
|
||||
Weekday string
|
||||
Month string
|
||||
|
||||
ID uint
|
||||
now time.Time
|
||||
lastExecutionTime time.Time
|
||||
nextExecutionTime time.Time
|
||||
|
||||
second, minute, hour, dom, month, dow uint64
|
||||
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (j *Job) Format() string {
|
||||
return fmt.Sprintf("second: %s minute: %s hour: %s day: %s weekday: %s month: %s",
|
||||
j.Second, j.Minute, j.Hour, j.Day, j.Weekday, j.Month)
|
||||
}
|
||||
func (j *Job) GetNextExecTime() time.Time {
|
||||
return j.nextExecutionTime
|
||||
}
|
||||
|
||||
func (j *Job) GetLastExecTime() time.Time {
|
||||
return j.lastExecutionTime
|
||||
}
|
||||
|
||||
// parse 解析定时规则
|
||||
// 根据规则生成符和条件的日期
|
||||
// 例如:*/2 如果位于分位,则生成0,2,4,6....58
|
||||
// 生成的日期逐条的被映射到uint64数值中
|
||||
// min |= 1<<2
|
||||
func (j *Job) parse() error {
|
||||
var err error
|
||||
field := func(field string, r bounds) uint64 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var bits uint64
|
||||
bits, err = getField(field, r)
|
||||
return bits
|
||||
}
|
||||
j.second = field(j.Second, seconds)
|
||||
j.minute = field(j.Minute, minutes)
|
||||
j.hour = field(j.Hour, hours)
|
||||
j.dom = field(j.Day, dom)
|
||||
j.month = field(j.Month, months)
|
||||
j.dow = field(j.Weekday, dow)
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// NextExecTime 获得下次执行时间
|
||||
func (j *Job) NextExecutionTime(t time.Time) (time.Time, error) {
|
||||
if err := j.parse(); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
added := false
|
||||
defer func() {
|
||||
j.lastExecutionTime, j.nextExecutionTime = j.nextExecutionTime, t
|
||||
}()
|
||||
|
||||
// 设置最大调度周期为5年
|
||||
yearLimit := t.Year() + 5
|
||||
|
||||
WRAP:
|
||||
if t.Year() > yearLimit {
|
||||
return time.Time{}, errors.New("Over 5 years")
|
||||
}
|
||||
|
||||
for 1<<uint(t.Month())&j.month == 0 {
|
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added {
|
||||
added = true
|
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.AddDate(0, 1, 0)
|
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
// Now get a day in that month.
|
||||
for !dayMatches(j, t) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.AddDate(0, 0, 1)
|
||||
|
||||
if t.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Hour())&j.hour == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.Add(1 * time.Hour)
|
||||
|
||||
if t.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Minute())&j.minute == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Minute)
|
||||
}
|
||||
t = t.Add(1 * time.Minute)
|
||||
|
||||
if t.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Second())&j.second == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Second)
|
||||
}
|
||||
t = t.Add(1 * time.Second)
|
||||
|
||||
if t.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func dayMatches(j *Job, t time.Time) bool {
|
||||
|
||||
if j.Day == "L" {
|
||||
l := util.CountDaysOfMonth(t.Year(), int(t.Month()))
|
||||
j.dom = getBits(uint(l), uint(l), 1)
|
||||
}
|
||||
|
||||
var (
|
||||
domMatch bool = 1<<uint(t.Day())&j.dom > 0
|
||||
dowMatch bool = 1<<uint(t.Weekday())&j.dow > 0
|
||||
)
|
||||
|
||||
if j.dom&starBit > 0 || j.dow&starBit > 0 {
|
||||
return domMatch && dowMatch
|
||||
}
|
||||
return domMatch || dowMatch
|
||||
}
|
||||
70
pkg/crontab/job_test.go
Normal file
70
pkg/crontab/job_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package crontab
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/test"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJob_NextExecutionTime(t *testing.T) {
|
||||
timeLayout := "2006-01-02 15:04:05"
|
||||
j := &Job{
|
||||
Second: "48",
|
||||
Minute: "3",
|
||||
Hour: "12",
|
||||
Day: "25",
|
||||
Weekday: "*",
|
||||
Month: "1",
|
||||
}
|
||||
|
||||
tt, err := j.NextExecutionTime(time.Now())
|
||||
test.Nil(t, err)
|
||||
test.Equal(t, "2020-01-25 12:03:48", tt.Format(timeLayout))
|
||||
|
||||
tt, err = j.NextExecutionTime(tt)
|
||||
test.Nil(t, err)
|
||||
test.Equal(t, "2021-01-25 12:03:48", tt.Format(timeLayout))
|
||||
|
||||
tt, err = j.NextExecutionTime(tt)
|
||||
test.Equal(t, "2022-01-25 12:03:48", tt.Format(timeLayout))
|
||||
|
||||
j = &Job{
|
||||
Second: "58",
|
||||
Minute: "*/4",
|
||||
Hour: "12",
|
||||
Day: "4",
|
||||
Weekday: "*",
|
||||
Month: "3",
|
||||
}
|
||||
tt, err = j.NextExecutionTime(time.Now())
|
||||
test.Nil(t, err)
|
||||
test.Equal(t, "2020-03-04 12:00:58", tt.Format(timeLayout))
|
||||
|
||||
tt, err = j.NextExecutionTime(tt)
|
||||
test.Nil(t, err)
|
||||
test.Equal(t, "2020-03-04 12:04:58", tt.Format(timeLayout))
|
||||
|
||||
tt, err = j.NextExecutionTime(tt)
|
||||
test.Nil(t, err)
|
||||
test.Equal(t, "2020-03-04 12:08:58", tt.Format(timeLayout))
|
||||
|
||||
j = &Job{
|
||||
Second: "0",
|
||||
Minute: "*",
|
||||
Hour: "*",
|
||||
Day: "*",
|
||||
Weekday: "*",
|
||||
Month: "*",
|
||||
}
|
||||
|
||||
tt, err = j.NextExecutionTime(time.Now())
|
||||
test.Nil(t, err)
|
||||
t.Log(tt, j.GetLastExecTime())
|
||||
for i := 0; i < 1000; i++ {
|
||||
tt, err = j.NextExecutionTime(tt)
|
||||
test.Nil(t, err)
|
||||
t.Log(tt, j.GetLastExecTime())
|
||||
}
|
||||
|
||||
t.Log("end")
|
||||
}
|
||||
128
pkg/crontab/parse.go
Normal file
128
pkg/crontab/parse.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package crontab
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getRange(expr string, r bounds) (uint64, error) {
|
||||
var (
|
||||
start, end, step uint
|
||||
rangeAndStep = strings.Split(expr, "/")
|
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-")
|
||||
singleDigit = len(lowAndHigh) == 1
|
||||
err error
|
||||
)
|
||||
|
||||
var extra uint64
|
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
|
||||
start = r.min
|
||||
end = r.max
|
||||
extra = starBit
|
||||
} else {
|
||||
if lowAndHigh[0] == "L" {
|
||||
return 0, nil
|
||||
}
|
||||
start, err = parseIntOrName(lowAndHigh[0], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch len(lowAndHigh) {
|
||||
case 1:
|
||||
end = start
|
||||
case 2:
|
||||
end, err = parseIntOrName(lowAndHigh[1], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("Too many hyphens: %s", expr)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(rangeAndStep) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
step, err = mustParseInt(rangeAndStep[1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit {
|
||||
end = r.max
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("Too many slashes: %s", expr)
|
||||
}
|
||||
|
||||
if start < r.min {
|
||||
return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
|
||||
}
|
||||
if end > r.max {
|
||||
return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
|
||||
}
|
||||
if start > end {
|
||||
return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
|
||||
}
|
||||
if step == 0 {
|
||||
return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
|
||||
}
|
||||
|
||||
return getBits(start, end, step) | extra, nil
|
||||
}
|
||||
|
||||
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
|
||||
if names != nil {
|
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok {
|
||||
return namedInt, nil
|
||||
}
|
||||
}
|
||||
return mustParseInt(expr)
|
||||
}
|
||||
|
||||
// mustParseInt parses the given expression as an int or returns an error.
|
||||
func mustParseInt(expr string) (uint, error) {
|
||||
num, err := strconv.Atoi(expr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
|
||||
}
|
||||
if num < 0 {
|
||||
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
|
||||
}
|
||||
|
||||
return uint(num), nil
|
||||
}
|
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 {
|
||||
var bits uint64
|
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 {
|
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
|
||||
}
|
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step {
|
||||
bits |= 1 << i
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
func getField(field string, r bounds) (uint64, error) {
|
||||
var bits uint64
|
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
|
||||
for _, expr := range ranges {
|
||||
bit, err := getRange(expr, r)
|
||||
if err != nil {
|
||||
return bits, err
|
||||
}
|
||||
bits |= bit
|
||||
}
|
||||
return bits, nil
|
||||
}
|
||||
134
pkg/file/file.go
Normal file
134
pkg/file/file.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
func Exist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil || os.IsExist(err)
|
||||
}
|
||||
|
||||
func GetCurrentDirectory() string {
|
||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ""
|
||||
}
|
||||
return filepath.Clean(strings.Replace(dir, "\\", "/", -1))
|
||||
|
||||
}
|
||||
|
||||
func IsTextFile(data []byte) bool {
|
||||
if len(data) == 0 {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(http.DetectContentType(data), "text/")
|
||||
}
|
||||
func IsImageFile(data []byte) bool {
|
||||
return strings.Contains(http.DetectContentType(data), "image/")
|
||||
}
|
||||
func IsPDFFile(data []byte) bool {
|
||||
return strings.Contains(http.DetectContentType(data), "application/pdf")
|
||||
}
|
||||
func IsVideoFile(data []byte) bool {
|
||||
return strings.Contains(http.DetectContentType(data), "video/")
|
||||
}
|
||||
|
||||
const (
|
||||
Byte = 1
|
||||
KByte = Byte * 1024
|
||||
MByte = KByte * 1024
|
||||
GByte = MByte * 1024
|
||||
TByte = GByte * 1024
|
||||
PByte = TByte * 1024
|
||||
EByte = PByte * 1024
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kb": KByte,
|
||||
"mb": MByte,
|
||||
"gb": GByte,
|
||||
"tb": TByte,
|
||||
"pb": PByte,
|
||||
"eb": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := float64(s) / math.Pow(base, math.Floor(e))
|
||||
f := "%.0f"
|
||||
if val < 10 {
|
||||
f = "%.1f"
|
||||
}
|
||||
return fmt.Sprintf(f+" %s", val, suffix)
|
||||
}
|
||||
func FileSize(s int64) string {
|
||||
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(uint64(s), 1024, sizes)
|
||||
}
|
||||
|
||||
func CreateFile(path string) (*os.File, error) {
|
||||
err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Create(path)
|
||||
}
|
||||
|
||||
func DirSize(dir string) int64 {
|
||||
var total int64
|
||||
filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
total += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
func Remove(dir string, t time.Time) (total int64, size int64, err error) {
|
||||
err = filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return errors.New(fpath + " not exists")
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if info.ModTime().Before(t) {
|
||||
total++
|
||||
size += info.Size()
|
||||
err = os.Remove(fpath)
|
||||
}
|
||||
} else {
|
||||
// 删除空目录
|
||||
err = os.Remove(fpath)
|
||||
if err == nil {
|
||||
total++
|
||||
log.Println("delete ", fpath)
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
220
pkg/finder/finder.go
Normal file
220
pkg/finder/finder.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"jiacrontab/pkg/file"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type matchDataChunk struct {
|
||||
modifyTime time.Time
|
||||
matchData []byte
|
||||
}
|
||||
|
||||
type DataQueue []matchDataChunk
|
||||
|
||||
func (d DataQueue) Swap(i, j int) {
|
||||
d[i], d[j] = d[j], d[i]
|
||||
}
|
||||
func (d DataQueue) Less(i, j int) bool {
|
||||
return d[i].modifyTime.Unix() < d[j].modifyTime.Unix()
|
||||
}
|
||||
func (d DataQueue) Len() int {
|
||||
return len(d)
|
||||
}
|
||||
|
||||
type Finder struct {
|
||||
matchDataQueue DataQueue
|
||||
curr int32
|
||||
regexp *regexp.Regexp
|
||||
pagesize int
|
||||
errors []error
|
||||
patternAll bool
|
||||
filter func(os.FileInfo) bool
|
||||
isTail bool
|
||||
offset int64
|
||||
fileSize int64
|
||||
}
|
||||
|
||||
func NewFinder(filter func(os.FileInfo) bool) *Finder {
|
||||
return &Finder{
|
||||
filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
func (fd *Finder) SetTail(flag bool) {
|
||||
fd.isTail = flag
|
||||
}
|
||||
|
||||
func (fd *Finder) Offset() int64 {
|
||||
return fd.offset
|
||||
}
|
||||
|
||||
func (fd *Finder) HumanateFileSize() string {
|
||||
return file.FileSize(fd.fileSize)
|
||||
}
|
||||
|
||||
func (fd *Finder) FileSize() int64 {
|
||||
return fd.fileSize
|
||||
}
|
||||
|
||||
func (fd *Finder) find(fpath string, modifyTime time.Time) error {
|
||||
|
||||
var matchData []byte
|
||||
var reader *bufio.Reader
|
||||
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd.fileSize = info.Size()
|
||||
|
||||
if fd.fileSize < fd.offset {
|
||||
return errors.New("out of file")
|
||||
}
|
||||
|
||||
if fd.isTail {
|
||||
if fd.offset < 0 {
|
||||
fd.offset = fd.fileSize
|
||||
}
|
||||
f.Seek(fd.offset, 0)
|
||||
reader = bufio.NewReader(NewTailReader(f, fd.offset))
|
||||
} else {
|
||||
f.Seek(fd.offset, 0)
|
||||
reader = bufio.NewReader(f)
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
bts, _ := reader.ReadBytes('\n')
|
||||
|
||||
if len(bts) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if fd.isTail {
|
||||
fd.offset -= int64(len(bts))
|
||||
} else {
|
||||
fd.offset += int64(len(bts))
|
||||
}
|
||||
|
||||
if fd.isTail {
|
||||
if fd.offset == 0 {
|
||||
bts = append(bts, '\n')
|
||||
}
|
||||
invert(bts)
|
||||
}
|
||||
|
||||
if fd.patternAll || fd.regexp.Match(bts) {
|
||||
matchData = append(matchData, bts...)
|
||||
fd.curr++
|
||||
}
|
||||
|
||||
if fd.curr >= int32(fd.pagesize) {
|
||||
break
|
||||
}
|
||||
|
||||
if fd.offset <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(matchData) > 0 {
|
||||
fd.matchDataQueue = append(fd.matchDataQueue, matchDataChunk{
|
||||
modifyTime: modifyTime,
|
||||
matchData: bytes.TrimLeft(bytes.TrimRight(matchData, "\n"), "\n"),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *Finder) walkFunc(fpath string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
if fd.filter != nil && fd.filter(info) {
|
||||
err := fd.find(fpath, info.ModTime())
|
||||
if err != nil {
|
||||
fd.errors = append(fd.errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *Finder) Search(root string, expr string, data *[]byte, offset int64, pagesize int) error {
|
||||
var err error
|
||||
fd.pagesize = pagesize
|
||||
fd.offset = offset
|
||||
|
||||
if expr == "" {
|
||||
fd.patternAll = true
|
||||
}
|
||||
|
||||
if !file.Exist(root) {
|
||||
return errors.New(root + " not exist")
|
||||
}
|
||||
|
||||
fd.regexp, err = regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filepath.Walk(root, fd.walkFunc)
|
||||
|
||||
sort.Stable(fd.matchDataQueue)
|
||||
for _, v := range fd.matchDataQueue {
|
||||
*data = append(*data, v.matchData...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *Finder) GetErrors() []error {
|
||||
return fd.errors
|
||||
}
|
||||
|
||||
func SearchAndDeleteFileOnDisk(dir string, d time.Duration, size int64) {
|
||||
t := time.NewTicker(1 * time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return errors.New(fpath + "not exists")
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if time.Now().Sub(info.ModTime()) > d {
|
||||
err = os.Remove(fpath)
|
||||
|
||||
}
|
||||
|
||||
if info.Size() > size && size != 0 {
|
||||
err = os.Remove(fpath)
|
||||
|
||||
}
|
||||
} else {
|
||||
// 删除空目录
|
||||
err := os.Remove(fpath)
|
||||
if err == nil {
|
||||
log.Println("delete ", fpath)
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
53
pkg/finder/reader.go
Normal file
53
pkg/finder/reader.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type TailReader struct {
|
||||
f *os.File
|
||||
curr int64
|
||||
isEOF bool
|
||||
}
|
||||
|
||||
func (t *TailReader) Read(b []byte) (n int, err error) {
|
||||
if t.isEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
off := t.curr - int64(len(b))
|
||||
if off < 0 {
|
||||
off = 0
|
||||
n, err = t.f.ReadAt(b[0:t.curr], off)
|
||||
} else {
|
||||
t.curr = off
|
||||
n, err = t.f.ReadAt(b, off)
|
||||
}
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
return n, err
|
||||
}
|
||||
|
||||
invert(b[0:n])
|
||||
|
||||
if off == 0 {
|
||||
t.isEOF = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewTailReader(f *os.File, offset int64) io.Reader {
|
||||
return &TailReader{
|
||||
f: f,
|
||||
curr: offset,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func invert(b []byte) {
|
||||
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
||||
}
|
||||
38
pkg/kproc/proc.go
Normal file
38
pkg/kproc/proc.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package kproc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"jiacrontab/pkg/file"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type KCmd struct {
|
||||
ctx context.Context
|
||||
*exec.Cmd
|
||||
isKillChildProcess bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// SetEnv 设置环境变量
|
||||
func (k *KCmd) SetEnv(env []string) {
|
||||
if len(env) == 0 {
|
||||
return
|
||||
}
|
||||
k.Cmd.Env = env
|
||||
}
|
||||
|
||||
// SetDir 设置工作目录
|
||||
func (k *KCmd) SetDir(dir string) {
|
||||
if dir == "" {
|
||||
return
|
||||
}
|
||||
if file.Exist(dir) == false {
|
||||
return
|
||||
}
|
||||
k.Cmd.Dir = dir
|
||||
}
|
||||
|
||||
// SetExitKillChildProcess 设置主进程退出时是否kill子进程,默认kill
|
||||
func (k *KCmd) SetExitKillChildProcess(ok bool) {
|
||||
k.isKillChildProcess = ok
|
||||
}
|
||||
78
pkg/kproc/proc_posix.go
Normal file
78
pkg/kproc/proc_posix.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// +build !windows
|
||||
|
||||
package kproc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
func CommandContext(ctx context.Context, name string, arg ...string) *KCmd {
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
cmd.SysProcAttr.Setsid = true
|
||||
return &KCmd{
|
||||
ctx: ctx,
|
||||
Cmd: cmd,
|
||||
isKillChildProcess: true,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetUser 设置执行用户要保证root权限
|
||||
func (k *KCmd) SetUser(username string) {
|
||||
if username == "" {
|
||||
return
|
||||
}
|
||||
u, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
log.Error("setUser error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("KCmd set uid=%s,gid=%s", u.Uid, u.Gid)
|
||||
k.SysProcAttr = &syscall.SysProcAttr{}
|
||||
uid, _ := strconv.Atoi(u.Uid)
|
||||
gid, _ := strconv.Atoi(u.Gid)
|
||||
k.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
|
||||
|
||||
}
|
||||
|
||||
func (k *KCmd) KillAll() {
|
||||
|
||||
select {
|
||||
case k.done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
if k.Process == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if k.isKillChildProcess == false {
|
||||
return
|
||||
}
|
||||
|
||||
group, err := os.FindProcess(-k.Process.Pid)
|
||||
if err == nil {
|
||||
group.Signal(syscall.SIGKILL)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KCmd) Wait() error {
|
||||
defer k.KillAll()
|
||||
go func() {
|
||||
select {
|
||||
case <-k.ctx.Done():
|
||||
k.KillAll()
|
||||
case <-k.done:
|
||||
}
|
||||
}()
|
||||
return k.Cmd.Wait()
|
||||
}
|
||||
51
pkg/kproc/proc_windows.go
Normal file
51
pkg/kproc/proc_windows.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package kproc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func CommandContext(ctx context.Context, name string, arg ...string) *KCmd {
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
return &KCmd{
|
||||
Cmd: cmd,
|
||||
ctx: ctx,
|
||||
isKillChildProcess: true,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KCmd) SetUser(username string) {
|
||||
// TODO:windows切换用户
|
||||
}
|
||||
|
||||
func (k *KCmd) KillAll() {
|
||||
select {
|
||||
case k.done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
if k.Process == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if k.isKillChildProcess == false {
|
||||
return
|
||||
}
|
||||
|
||||
c := exec.Command("taskkill", "/t", "/f", "/pid", fmt.Sprint(k.Process.Pid))
|
||||
c.Stdout = k.Cmd.Stdout
|
||||
c.Stderr = k.Cmd.Stderr
|
||||
}
|
||||
|
||||
func (k *KCmd) Wait() error {
|
||||
defer k.KillAll()
|
||||
go func() {
|
||||
select {
|
||||
case <-k.ctx.Done():
|
||||
k.KillAll()
|
||||
case <-k.done:
|
||||
}
|
||||
}()
|
||||
return k.Cmd.Wait()
|
||||
}
|
||||
31
pkg/mailer/login.go
Normal file
31
pkg/mailer/login.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
type loginAuth struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
// SMTP AUTH LOGIN Auth Handler
|
||||
func LoginAuth(username, password string) smtp.Auth {
|
||||
return &loginAuth{username, password}
|
||||
}
|
||||
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
return "LOGIN", []byte{}, nil
|
||||
}
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(a.username), nil
|
||||
case "Password:":
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknwon fromServer: %s", string(fromServer))
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
204
pkg/mailer/mail.go
Normal file
204
pkg/mailer/mail.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
MailConfig *Mailer
|
||||
mailQueue chan *Message
|
||||
)
|
||||
|
||||
type Mailer struct {
|
||||
QueueLength int
|
||||
SubjectPrefix string
|
||||
Host string
|
||||
From string
|
||||
FromEmail string
|
||||
User, Passwd string
|
||||
DisableHelo bool
|
||||
HeloHostname string
|
||||
SkipVerify bool
|
||||
UseCertificate bool
|
||||
CertFile, KeyFile string
|
||||
UsePlainText bool
|
||||
HookMode bool
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
*gomail.Message
|
||||
Info string
|
||||
confirmChan chan struct{}
|
||||
}
|
||||
|
||||
func NewMessage(to []string, subject, htmlBody string) *Message {
|
||||
return NewMessageFrom(to, MailConfig.From, subject, htmlBody)
|
||||
}
|
||||
|
||||
func NewMessageFrom(to []string, from, subject, htmlBody string) *Message {
|
||||
log.Printf("QueueLength (%d) NewMessage (htmlBody) \n%s\n", len(mailQueue), htmlBody)
|
||||
msg := gomail.NewMessage()
|
||||
msg.SetHeader("From", from)
|
||||
msg.SetHeader("To", to...)
|
||||
msg.SetHeader("Subject", subject)
|
||||
msg.SetDateHeader("Date", time.Now())
|
||||
contentType := "text/html"
|
||||
|
||||
msg.SetBody(contentType, htmlBody)
|
||||
return &Message{
|
||||
Message: msg,
|
||||
confirmChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
}
|
||||
|
||||
func (s *Sender) Send(from string, to []string, msg io.WriterTo) error {
|
||||
host, port, err := net.SplitHostPort(MailConfig.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: MailConfig.SkipVerify,
|
||||
ServerName: host,
|
||||
}
|
||||
if MailConfig.UseCertificate {
|
||||
cert, err := tls.LoadX509KeyPair(MailConfig.CertFile, MailConfig.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 3*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
isSecureConn := false
|
||||
if strings.HasSuffix(port, "465") {
|
||||
conn = tls.Client(conn, tlsConfig)
|
||||
isSecureConn = true
|
||||
}
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewClient: %v", err)
|
||||
}
|
||||
|
||||
if MailConfig.DisableHelo {
|
||||
hostname := MailConfig.HeloHostname
|
||||
if len(hostname) == 0 {
|
||||
hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Hello(hostname); err != nil {
|
||||
return fmt.Errorf("Hello:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
hasStartTLS, _ := client.Extension("STARTTLS")
|
||||
if !isSecureConn && hasStartTLS {
|
||||
if err = client.StartTLS(tlsConfig); err != nil {
|
||||
return fmt.Errorf("StartTLS:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
canAuth, options := client.Extension("AUTH")
|
||||
|
||||
if canAuth && len(MailConfig.User) > 0 {
|
||||
var auth smtp.Auth
|
||||
if strings.Contains(options, "CRAM-MD5") {
|
||||
auth = smtp.CRAMMD5Auth(MailConfig.User, MailConfig.Passwd)
|
||||
} else if strings.Contains(options, "PLAIN") {
|
||||
auth = smtp.PlainAuth("", MailConfig.User, MailConfig.Passwd, host)
|
||||
} else if strings.Contains(options, "LOGIN") {
|
||||
// Patch for AUTH LOGIN
|
||||
auth = LoginAuth(MailConfig.User, MailConfig.Passwd)
|
||||
}
|
||||
if auth != nil {
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("Auth: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Mail(from); err != nil {
|
||||
return fmt.Errorf("Mail: %v", err)
|
||||
}
|
||||
for _, rec := range to {
|
||||
if err = client.Rcpt(rec); err != nil {
|
||||
return fmt.Errorf("Rcpt: %v", err)
|
||||
}
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Data: %v", err)
|
||||
} else if _, err = msg.WriteTo(w); err != nil {
|
||||
return fmt.Errorf("WriteTo: %v", err)
|
||||
} else if err = w.Close(); err != nil {
|
||||
return fmt.Errorf("Close: %v", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func processMailQueue() {
|
||||
sender := &Sender{}
|
||||
for {
|
||||
select {
|
||||
case msg := <-mailQueue:
|
||||
if err := gomail.Send(sender, msg.Message); err != nil {
|
||||
log.Errorf("Fail to send emails %s: %s - %v\n", msg.GetHeader("To"), msg.Info, err)
|
||||
} else {
|
||||
log.Infof("E-mails sent %s: %s\n", msg.GetHeader("To"), msg.Info)
|
||||
}
|
||||
msg.confirmChan <- struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InitMailer(m *Mailer) {
|
||||
MailConfig = m
|
||||
if MailConfig == nil || mailQueue != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mailQueue = make(chan *Message, MailConfig.QueueLength)
|
||||
go processMailQueue()
|
||||
}
|
||||
|
||||
func Send(msg *Message) {
|
||||
mailQueue <- msg
|
||||
if MailConfig.HookMode {
|
||||
<-msg.confirmChan
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-msg.confirmChan
|
||||
}()
|
||||
}
|
||||
|
||||
func SendMail(to []string, subject, content string) error {
|
||||
if MailConfig == nil {
|
||||
return errors.New("update mail config must restart service")
|
||||
}
|
||||
msg := NewMessage(to, subject, content)
|
||||
Send(msg)
|
||||
return nil
|
||||
}
|
||||
75
pkg/pprof/pprof.go
Normal file
75
pkg/pprof/pprof.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package pprof
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/file"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
func ListenPprof() {
|
||||
go listenSignal()
|
||||
}
|
||||
|
||||
func cpuprofile() {
|
||||
path := filepath.Join("pprof", "cpuprofile")
|
||||
log.Debugf("profile save in %s", path)
|
||||
|
||||
f, err := file.CreateFile(path)
|
||||
if err != nil {
|
||||
log.Error("could not create CPU profile: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Error("could not start CPU profile: ", err)
|
||||
} else {
|
||||
time.Sleep(time.Minute)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
func memprofile() {
|
||||
path := filepath.Join("pprof", "memprofile")
|
||||
log.Debugf("profile save in %s", path)
|
||||
f, err := file.CreateFile(path)
|
||||
if err != nil {
|
||||
log.Error("could not create memory profile: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
runtime.GC() // get up-to-date statistics
|
||||
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Error("could not write memory profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func profile() {
|
||||
names := []string{
|
||||
"goroutine",
|
||||
"heap",
|
||||
"allocs",
|
||||
"threadcreate",
|
||||
"block",
|
||||
"mutex",
|
||||
}
|
||||
for _, name := range names {
|
||||
path := filepath.Join("pprof", name)
|
||||
log.Debugf("profile save in %s", path)
|
||||
f, err := file.CreateFile(path)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
pprof.Lookup(name).WriteTo(f, 0)
|
||||
}
|
||||
|
||||
}
|
||||
20
pkg/pprof/pprof_posix.go
Normal file
20
pkg/pprof/pprof_posix.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// +build !windows
|
||||
|
||||
package pprof
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func listenSignal() {
|
||||
signChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signChan, syscall.SIGUSR1)
|
||||
for {
|
||||
<-signChan
|
||||
profile()
|
||||
memprofile()
|
||||
cpuprofile()
|
||||
}
|
||||
}
|
||||
4
pkg/pprof/pprof_windows.go
Normal file
4
pkg/pprof/pprof_windows.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package pprof
|
||||
|
||||
func listenSignal() {
|
||||
}
|
||||
89
pkg/pqueue/pqueue.go
Normal file
89
pkg/pqueue/pqueue.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Package pqueue jiacrontab中使用的优先队列
|
||||
// 参考nsq的实现
|
||||
// 做了注释和少量调整
|
||||
package pqueue
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Value interface{}
|
||||
Priority int64
|
||||
Index int
|
||||
}
|
||||
|
||||
// PriorityQueue 最小堆实现的优先队列
|
||||
type PriorityQueue []*Item
|
||||
|
||||
// New 创建
|
||||
func New(capacity int) PriorityQueue {
|
||||
return make(PriorityQueue, 0, capacity)
|
||||
}
|
||||
|
||||
// Len 队列长队
|
||||
func (pq PriorityQueue) Len() int {
|
||||
return len(pq)
|
||||
}
|
||||
|
||||
// Less 比较相邻两个原素优先级
|
||||
func (pq PriorityQueue) Less(i, j int) bool {
|
||||
return pq[i].Priority < pq[j].Priority
|
||||
}
|
||||
|
||||
// Swap 交换相邻原素
|
||||
func (pq PriorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
pq[i].Index = i
|
||||
pq[j].Index = j
|
||||
}
|
||||
|
||||
// Push 添加新的item
|
||||
func (pq *PriorityQueue) Push(x interface{}) {
|
||||
n := len(*pq)
|
||||
c := cap(*pq)
|
||||
if n+1 > c {
|
||||
npq := make(PriorityQueue, n, c*2)
|
||||
copy(npq, *pq)
|
||||
*pq = npq
|
||||
}
|
||||
*pq = (*pq)[0 : n+1]
|
||||
item := x.(*Item)
|
||||
item.Index = n
|
||||
(*pq)[n] = item
|
||||
}
|
||||
|
||||
func (pq *PriorityQueue) update(item *Item, value string, priority int64) {
|
||||
item.Value = value
|
||||
item.Priority = priority
|
||||
heap.Fix(pq, item.Index)
|
||||
}
|
||||
|
||||
// Pop 弹出队列末端原素
|
||||
func (pq *PriorityQueue) Pop() interface{} {
|
||||
n := len(*pq)
|
||||
c := cap(*pq)
|
||||
if n < (c/2) && c > 25 {
|
||||
npq := make(PriorityQueue, n, c/2)
|
||||
copy(npq, *pq)
|
||||
*pq = npq
|
||||
}
|
||||
item := (*pq)[n-1]
|
||||
item.Index = -1
|
||||
*pq = (*pq)[0 : n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
// PeekAndShift 根据比较max并弹出原素
|
||||
func (pq *PriorityQueue) PeekAndShift(max int64) (*Item, int64) {
|
||||
if pq.Len() == 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
item := (*pq)[0]
|
||||
if item.Priority > max {
|
||||
return nil, item.Priority - max
|
||||
}
|
||||
heap.Remove(pq, 0)
|
||||
return item, 0
|
||||
}
|
||||
79
pkg/pqueue/pqueue_test.go
Normal file
79
pkg/pqueue/pqueue_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package pqueue
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func equal(t *testing.T, act, exp interface{}) {
|
||||
if !reflect.DeepEqual(exp, act) {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
t.Logf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n",
|
||||
filepath.Base(file), line, exp, act)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPriorityQueue(t *testing.T) {
|
||||
c := 100
|
||||
pq := New(c)
|
||||
|
||||
for i := 0; i < c+1; i++ {
|
||||
heap.Push(&pq, &Item{Value: i, Priority: int64(i)})
|
||||
}
|
||||
equal(t, pq.Len(), c+1)
|
||||
equal(t, cap(pq), c*2)
|
||||
|
||||
for i := 0; i < c+1; i++ {
|
||||
item := heap.Pop(&pq)
|
||||
equal(t, item.(*Item).Value.(int), i)
|
||||
}
|
||||
equal(t, cap(pq), c/4)
|
||||
}
|
||||
|
||||
func TestUnsortedInsert(t *testing.T) {
|
||||
c := 100
|
||||
pq := New(c)
|
||||
ints := make([]int, 0, c)
|
||||
|
||||
for i := 0; i < c; i++ {
|
||||
v := rand.Int()
|
||||
ints = append(ints, v)
|
||||
heap.Push(&pq, &Item{Value: i, Priority: int64(v)})
|
||||
}
|
||||
equal(t, pq.Len(), c)
|
||||
equal(t, cap(pq), c)
|
||||
|
||||
sort.Sort(sort.IntSlice(ints))
|
||||
|
||||
for i := 0; i < c; i++ {
|
||||
item, _ := pq.PeekAndShift(int64(ints[len(ints)-1]))
|
||||
equal(t, item.Priority, int64(ints[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
c := 100
|
||||
pq := New(c)
|
||||
|
||||
for i := 0; i < c; i++ {
|
||||
v := rand.Int()
|
||||
heap.Push(&pq, &Item{Value: "test", Priority: int64(v)})
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
heap.Remove(&pq, rand.Intn((c-1)-i))
|
||||
}
|
||||
|
||||
lastPriority := heap.Pop(&pq).(*Item).Priority
|
||||
for i := 0; i < (c - 10 - 1); i++ {
|
||||
item := heap.Pop(&pq)
|
||||
equal(t, lastPriority < item.(*Item).Priority, true)
|
||||
lastPriority = item.(*Item).Priority
|
||||
}
|
||||
}
|
||||
18
pkg/proto/apicode.go
Normal file
18
pkg/proto/apicode.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package proto
|
||||
|
||||
const (
|
||||
Code_Success = 0
|
||||
Code_FailedAuth = 5001
|
||||
Code_Error = 5002
|
||||
Code_NotFound = 5004
|
||||
Code_NotAllowed = 5005
|
||||
Code_JWTError = 5006
|
||||
Code_RPCError = 5007
|
||||
Code_ParamsError = 5008
|
||||
Code_DBError = 5009
|
||||
)
|
||||
|
||||
const (
|
||||
Msg_NotAllowed = "permission not allowed"
|
||||
Msg_JWTError = "parse jwt token failed"
|
||||
)
|
||||
66
pkg/proto/args.go
Normal file
66
pkg/proto/args.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"jiacrontab/models"
|
||||
)
|
||||
|
||||
type SearchLog struct {
|
||||
JobID uint
|
||||
GroupID uint
|
||||
UserID uint
|
||||
Root bool
|
||||
IsTail bool
|
||||
Offset int64
|
||||
Pagesize int
|
||||
Date string
|
||||
Pattern string
|
||||
}
|
||||
|
||||
type CleanNodeLog struct {
|
||||
Unit string
|
||||
Offset int
|
||||
}
|
||||
|
||||
type CleanNodeLogRet struct {
|
||||
Total int64 `json:"total"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type SearchLogResult struct {
|
||||
Content []byte
|
||||
Offset int64
|
||||
FileSize int64
|
||||
}
|
||||
|
||||
type SendMail struct {
|
||||
MailTo []string
|
||||
Subject string
|
||||
Content string
|
||||
}
|
||||
|
||||
type ApiPost struct {
|
||||
Urls []string
|
||||
Data string
|
||||
}
|
||||
|
||||
type ExecCrontabJobReply struct {
|
||||
Job models.CrontabJob
|
||||
Content []byte
|
||||
}
|
||||
|
||||
type ActionJobsArgs struct {
|
||||
UserID uint
|
||||
Root bool
|
||||
GroupID uint
|
||||
JobIDs []uint
|
||||
}
|
||||
|
||||
type GetJobArgs struct {
|
||||
UserID uint
|
||||
GroupID uint
|
||||
Root bool
|
||||
JobID uint
|
||||
}
|
||||
type EmptyArgs struct{}
|
||||
|
||||
type EmptyReply struct{}
|
||||
10
pkg/proto/const.go
Normal file
10
pkg/proto/const.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package proto
|
||||
|
||||
const (
|
||||
DefaultTimeLayout = "2006-01-02 15:04:05"
|
||||
|
||||
TimeoutTrigger_CallApi = "CallApi"
|
||||
TimeoutTrigger_SendEmail = "SendEmail"
|
||||
TimeoutTrigger_Kill = "Kill"
|
||||
TimeoutTrigger_DingdingWebhook = "DingdingWebhook"
|
||||
)
|
||||
70
pkg/proto/crontab.go
Normal file
70
pkg/proto/crontab.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"jiacrontab/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DepJobs []DepJob
|
||||
type DepJob struct {
|
||||
Name string
|
||||
Dest string
|
||||
From string
|
||||
ProcessID int // 当前主任务进程id
|
||||
ID string // 依赖任务id
|
||||
JobID uint // 主任务id
|
||||
JobUniqueID string // 主任务唯一标志
|
||||
Commands []string
|
||||
Timeout int64
|
||||
Err error
|
||||
LogContent []byte
|
||||
}
|
||||
|
||||
type QueryJobArgs struct {
|
||||
SearchTxt string
|
||||
Root bool
|
||||
GroupID uint
|
||||
UserID uint
|
||||
Page, Pagesize int
|
||||
}
|
||||
|
||||
type QueryCrontabJobRet struct {
|
||||
Total int64
|
||||
Page int
|
||||
GroupID uint
|
||||
Pagesize int
|
||||
List []models.CrontabJob
|
||||
}
|
||||
|
||||
type QueryDaemonJobRet struct {
|
||||
Total int64
|
||||
GroupID int
|
||||
Page int
|
||||
Pagesize int
|
||||
List []models.DaemonJob
|
||||
}
|
||||
|
||||
type AuditJobArgs struct {
|
||||
GroupID uint
|
||||
Root bool
|
||||
UserID uint
|
||||
JobIDs []uint
|
||||
}
|
||||
|
||||
type CrontabApiNotifyBody struct {
|
||||
NodeAddr string
|
||||
JobName string
|
||||
JobID int
|
||||
CreateUsername string
|
||||
CreatedAt time.Time
|
||||
Timeout int64
|
||||
Type string
|
||||
RetryNum int
|
||||
}
|
||||
|
||||
type EditCrontabJobArgs struct {
|
||||
Job models.CrontabJob
|
||||
UserID uint
|
||||
GroupID uint
|
||||
Root bool
|
||||
}
|
||||
12
pkg/proto/daemon.go
Normal file
12
pkg/proto/daemon.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"jiacrontab/models"
|
||||
)
|
||||
|
||||
type EditDaemonJobArgs struct {
|
||||
Job models.DaemonJob
|
||||
GroupID uint
|
||||
UserID uint
|
||||
Root bool
|
||||
}
|
||||
14
pkg/proto/resp.go
Normal file
14
pkg/proto/resp.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package proto
|
||||
|
||||
const (
|
||||
SuccessRespCode = 0
|
||||
ErrorRespCode = -1
|
||||
)
|
||||
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Sign string `json:"sign"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
111
pkg/rpc/client.go
Normal file
111
pkg/rpc/client.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"jiacrontab/pkg/proto"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"time"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
const (
|
||||
diaTimeout = 5 * time.Second
|
||||
callTimeout = 1 * time.Minute
|
||||
pingDuration = 3 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRpc = errors.New("rpc is not available")
|
||||
ErrRpcTimeout = errors.New("rpc call timeout")
|
||||
ErrRpcCancel = errors.New("rpc call cancel")
|
||||
ErrShutdown = rpc.ErrShutdown
|
||||
)
|
||||
|
||||
type ClientOptions struct {
|
||||
Network string
|
||||
Addr string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*rpc.Client
|
||||
options ClientOptions
|
||||
quit chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
func Dial(options ClientOptions) (c *Client) {
|
||||
c = &Client{}
|
||||
c.options = options
|
||||
c.dial()
|
||||
c.quit = make(chan struct{}, 100)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) dial() (err error) {
|
||||
conn, err := net.DialTimeout(c.options.Network, c.options.Addr, diaTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Client = rpc.NewClient(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Call(serviceMethod string, ctx context.Context, args interface{}, reply interface{}) error {
|
||||
if serviceMethod != PingService && serviceMethod != RegisterService {
|
||||
log.Info("rpc call", c.options.Addr, serviceMethod)
|
||||
}
|
||||
|
||||
if c.Client == nil {
|
||||
return ErrRpc
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrRpcCancel
|
||||
case call := <-c.Client.Go(serviceMethod, args, reply, make(chan *rpc.Call, 1)).Done:
|
||||
return call.Error
|
||||
case <-time.After(callTimeout):
|
||||
return ErrRpcTimeout
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Error() error {
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
c.quit <- struct{}{}
|
||||
}
|
||||
|
||||
func (c *Client) Ping(serviceMethod string) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case <-c.quit:
|
||||
goto closed
|
||||
default:
|
||||
}
|
||||
if c.Client != nil && c.err == nil {
|
||||
if err = c.Call(serviceMethod, context.TODO(), &proto.EmptyArgs{}, &proto.EmptyReply{}); err != nil {
|
||||
c.err = err
|
||||
c.Client.Close()
|
||||
log.Infof("client.Call(%s, args, reply) error (%v) \n", serviceMethod, err)
|
||||
}
|
||||
} else {
|
||||
if err = c.dial(); err == nil {
|
||||
c.err = nil
|
||||
log.Info("client reconnet ", c.options.Addr)
|
||||
}
|
||||
}
|
||||
time.Sleep(pingDuration)
|
||||
}
|
||||
closed:
|
||||
log.Info("rpc quited", c.options.Addr)
|
||||
if c.Client != nil {
|
||||
c.Client.Close()
|
||||
}
|
||||
}
|
||||
66
pkg/rpc/client_test.go
Normal file
66
pkg/rpc/client_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/proto"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Logic struct {
|
||||
}
|
||||
|
||||
func (l *Logic) Ping(args *proto.EmptyArgs, reply *proto.EmptyReply) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Logic) Say(args string, reply *string) error {
|
||||
|
||||
*reply = "hello boy"
|
||||
time.Sleep(100 * time.Second)
|
||||
return nil
|
||||
}
|
||||
func TestCall(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
|
||||
done <- struct{}{}
|
||||
|
||||
log.Println("start server")
|
||||
err := listen(":6478", &Logic{})
|
||||
if err != nil {
|
||||
t.Fatal("server error:", err)
|
||||
}
|
||||
}()
|
||||
<-done
|
||||
time.Sleep(5 * time.Second)
|
||||
// 等待server启动
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
|
||||
defer wg.Done()
|
||||
var ret string
|
||||
// var args string
|
||||
err := Call(":6478", "Logic.Say", "", &ret)
|
||||
if err != nil {
|
||||
log.Println(i, "error:", err)
|
||||
}
|
||||
t.Log(i, ret)
|
||||
}(i)
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
t.Log("listen :6060")
|
||||
t.Log(http.ListenAndServe(":6060", nil))
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
log.Println("end")
|
||||
time.Sleep(2 * time.Minute)
|
||||
}
|
||||
86
pkg/rpc/clients.go
Normal file
86
pkg/rpc/clients.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/rpc"
|
||||
"sync"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultClients *clients
|
||||
PingService = "Srv.Ping"
|
||||
RegisterService = "Srv.Register"
|
||||
)
|
||||
|
||||
type clients struct {
|
||||
lock sync.RWMutex
|
||||
clients map[string]*Client
|
||||
}
|
||||
|
||||
func (c *clients) get(addr string) *Client {
|
||||
var (
|
||||
cli *Client
|
||||
ok bool
|
||||
op ClientOptions
|
||||
)
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cli, ok = c.clients[addr]; ok {
|
||||
return cli
|
||||
}
|
||||
op.Network = "tcp4"
|
||||
op.Addr = addr
|
||||
cli = Dial(op)
|
||||
c.clients[addr] = cli
|
||||
go cli.Ping(PingService)
|
||||
|
||||
return cli
|
||||
}
|
||||
|
||||
func (c *clients) del(addr string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cli, ok := c.clients[addr]; ok {
|
||||
cli.Close()
|
||||
}
|
||||
delete(c.clients, addr)
|
||||
}
|
||||
|
||||
func Call(addr string, serviceMethod string, args interface{}, reply interface{}) error {
|
||||
err := defaultClients.get(addr).Call(serviceMethod, context.TODO(), args, reply)
|
||||
if err == rpc.ErrShutdown {
|
||||
log.Debug("rpc remove", addr)
|
||||
Del(addr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func CallCtx(addr string, serviceMethod string, ctx context.Context, args interface{}, reply interface{}) error {
|
||||
err := defaultClients.get(addr).Call(serviceMethod, ctx, args, reply)
|
||||
if err == rpc.ErrShutdown {
|
||||
log.Debug("rpc remove", addr)
|
||||
Del(addr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Del(addr string) {
|
||||
if defaultClients != nil {
|
||||
defaultClients.del(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func DelNode(addr string) {
|
||||
if defaultClients != nil {
|
||||
defaultClients.del(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
defaultClients = &clients{
|
||||
clients: make(map[string]*Client),
|
||||
}
|
||||
}
|
||||
41
pkg/rpc/server.go
Normal file
41
pkg/rpc/server.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"github.com/iwannay/log"
|
||||
"net"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
// listen Start rpc server
|
||||
func listen(addr string, srcvr ...interface{}) error {
|
||||
var err error
|
||||
for _, v := range srcvr {
|
||||
if err = rpc.Register(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp4", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
log.Info("listen rpc", addr, "close")
|
||||
if err := l.Close(); err != nil {
|
||||
log.Infof("listen.Close() error(%v)", err)
|
||||
}
|
||||
}()
|
||||
|
||||
rpc.Accept(l)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenAndServe run rpc server
|
||||
func ListenAndServe(addr string, srcvr ...interface{}) {
|
||||
log.Info("rpc server listen:", addr)
|
||||
err := listen(addr, srcvr...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
58
pkg/test/assertions.go
Normal file
58
pkg/test/assertions.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Equal(t *testing.T, expected, actual interface{}) {
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
t.Logf("\033[31m%s:%d:\n\n\t %#v (expected)\n\n\t!= %#v (actual)\033[39m\n\n",
|
||||
filepath.Base(file), line, expected, actual)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func NotEqual(t *testing.T, expected, actual interface{}) {
|
||||
if reflect.DeepEqual(expected, actual) {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
t.Logf("\033[31m%s:%d:\n\n\tnexp: %#v\n\n\tgot: %#v\033[39m\n\n",
|
||||
filepath.Base(file), line, expected, actual)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func Nil(t *testing.T, object interface{}) {
|
||||
if !isNil(object) {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
t.Logf("\033[31m%s:%d:\n\n\t <nil> (expected)\n\n\t!= %#v (actual)\033[39m\n\n",
|
||||
filepath.Base(file), line, object)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func NotNil(t *testing.T, object interface{}) {
|
||||
if isNil(object) {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
t.Logf("\033[31m%s:%d:\n\n\tExpected value not to be <nil>\033[39m\n\n",
|
||||
filepath.Base(file), line)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func isNil(object interface{}) bool {
|
||||
if object == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(object)
|
||||
kind := value.Kind()
|
||||
if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
45
pkg/test/fakes.go
Normal file
45
pkg/test/fakes.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FakeNetConn struct {
|
||||
ReadFunc func([]byte) (int, error)
|
||||
WriteFunc func([]byte) (int, error)
|
||||
CloseFunc func() error
|
||||
LocalAddrFunc func() net.Addr
|
||||
RemoteAddrFunc func() net.Addr
|
||||
SetDeadlineFunc func(time.Time) error
|
||||
SetReadDeadlineFunc func(time.Time) error
|
||||
SetWriteDeadlineFunc func(time.Time) error
|
||||
}
|
||||
|
||||
func (f FakeNetConn) Read(b []byte) (int, error) { return f.ReadFunc(b) }
|
||||
func (f FakeNetConn) Write(b []byte) (int, error) { return f.WriteFunc(b) }
|
||||
func (f FakeNetConn) Close() error { return f.CloseFunc() }
|
||||
func (f FakeNetConn) LocalAddr() net.Addr { return f.LocalAddrFunc() }
|
||||
func (f FakeNetConn) RemoteAddr() net.Addr { return f.RemoteAddrFunc() }
|
||||
func (f FakeNetConn) SetDeadline(t time.Time) error { return f.SetDeadlineFunc(t) }
|
||||
func (f FakeNetConn) SetReadDeadline(t time.Time) error { return f.SetReadDeadlineFunc(t) }
|
||||
func (f FakeNetConn) SetWriteDeadline(t time.Time) error { return f.SetWriteDeadlineFunc(t) }
|
||||
|
||||
type fakeNetAddr struct{}
|
||||
|
||||
func (fakeNetAddr) Network() string { return "" }
|
||||
func (fakeNetAddr) String() string { return "" }
|
||||
|
||||
func NewFakeNetConn() FakeNetConn {
|
||||
netAddr := fakeNetAddr{}
|
||||
return FakeNetConn{
|
||||
ReadFunc: func(b []byte) (int, error) { return 0, nil },
|
||||
WriteFunc: func(b []byte) (int, error) { return len(b), nil },
|
||||
CloseFunc: func() error { return nil },
|
||||
LocalAddrFunc: func() net.Addr { return netAddr },
|
||||
RemoteAddrFunc: func() net.Addr { return netAddr },
|
||||
SetDeadlineFunc: func(time.Time) error { return nil },
|
||||
SetWriteDeadlineFunc: func(time.Time) error { return nil },
|
||||
SetReadDeadlineFunc: func(time.Time) error { return nil },
|
||||
}
|
||||
}
|
||||
22
pkg/test/logger.go
Normal file
22
pkg/test/logger.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package test
|
||||
|
||||
type Logger interface {
|
||||
Output(maxdepth int, s string) error
|
||||
}
|
||||
|
||||
type tbLog interface {
|
||||
Log(...interface{})
|
||||
}
|
||||
|
||||
type testLogger struct {
|
||||
tbLog
|
||||
}
|
||||
|
||||
func (tl *testLogger) Output(maxdepth int, s string) error {
|
||||
tl.Log(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTestLogger(tbl tbLog) Logger {
|
||||
return &testLogger{tbl}
|
||||
}
|
||||
10
pkg/util/arr.go
Normal file
10
pkg/util/arr.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
func FilterEmptyEle(in []string) (out []string) {
|
||||
for _, v := range in {
|
||||
if v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
173
pkg/util/fn.go
Normal file
173
pkg/util/fn.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"jiacrontab/pkg/file"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
|
||||
"github.com/iwannay/log"
|
||||
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func RandIntn(end int) int {
|
||||
return rand.Intn(end)
|
||||
}
|
||||
|
||||
func CurrentTime(t int64) string {
|
||||
if t == 0 {
|
||||
return "0"
|
||||
}
|
||||
return time.Unix(t, 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func SystemInfo(startTime time.Time) map[string]interface{} {
|
||||
var afterLastGC string
|
||||
goNum := runtime.NumGoroutine()
|
||||
cpuNum := runtime.NumCPU()
|
||||
mstat := &runtime.MemStats{}
|
||||
runtime.ReadMemStats(mstat)
|
||||
costTime := int(time.Since(startTime).Seconds())
|
||||
|
||||
if mstat.LastGC != 0 {
|
||||
afterLastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(mstat.LastGC))/1000/1000/1000)
|
||||
} else {
|
||||
afterLastGC = "0"
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"服务运行时间": fmt.Sprintf("%d天%d小时%d分%d秒", costTime/(3600*24), costTime%(3600*24)/3600, costTime%3600/60, costTime%(60)),
|
||||
"goroutine数量": goNum,
|
||||
"cpu核心数": cpuNum,
|
||||
|
||||
"当前内存使用量": file.FileSize(int64(mstat.Alloc)),
|
||||
"所有被分配的内存": file.FileSize(int64(mstat.TotalAlloc)),
|
||||
"内存占用量": file.FileSize(int64(mstat.Sys)),
|
||||
"指针查找次数": mstat.Lookups,
|
||||
"内存分配次数": mstat.Mallocs,
|
||||
"内存释放次数": mstat.Frees,
|
||||
"距离上次GC时间": afterLastGC,
|
||||
|
||||
// "当前 Heap 内存使用量": file.FileSize(int64(mstat.HeapAlloc)),
|
||||
// "Heap 内存占用量": file.FileSize(int64(mstat.HeapSys)),
|
||||
// "Heap 内存空闲量": file.FileSize(int64(mstat.HeapIdle)),
|
||||
// "正在使用的 Heap 内存": file.FileSize(int64(mstat.HeapInuse)),
|
||||
// "被释放的 Heap 内存": file.FileSize(int64(mstat.HeapReleased)),
|
||||
// "Heap 对象数量": mstat.HeapObjects,
|
||||
|
||||
"下次GC内存回收量": file.FileSize(int64(mstat.NextGC)),
|
||||
"GC暂停时间总量": fmt.Sprintf("%.3fs", float64(mstat.PauseTotalNs)/1000/1000/1000),
|
||||
"上次GC暂停时间": fmt.Sprintf("%.3fs", float64(mstat.PauseNs[(mstat.NumGC+255)%256])/1000/1000/1000),
|
||||
}
|
||||
}
|
||||
|
||||
func TryOpen(path string, flag int) (*os.File, error) {
|
||||
fabs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Errorf("TryOpen:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(fabs, flag, 0644)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(filepath.Dir(fabs), 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.OpenFile(fabs, flag, 0644)
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func CatFile(filepath string, limit int64, content *string) (isPath bool, err error) {
|
||||
f, err := os.Open(filepath)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if fi.Size() > limit {
|
||||
*content = filepath
|
||||
return true, nil
|
||||
}
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
*content = string(data)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ParseInt(i string) int {
|
||||
v, _ := strconv.Atoi(i)
|
||||
return v
|
||||
}
|
||||
|
||||
func ParseInt64(i string) int64 {
|
||||
v, _ := strconv.Atoi(i)
|
||||
return int64(v)
|
||||
}
|
||||
|
||||
func InArray(val interface{}, arr interface{}) bool {
|
||||
t := reflect.TypeOf(arr)
|
||||
v := reflect.ValueOf(arr)
|
||||
|
||||
if t.Kind() == reflect.Slice {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if v.Index(i).Interface() == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func UUID() string {
|
||||
|
||||
uu, err := uuid.NewGen().NewV1()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return fmt.Sprint(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
return uu.String()
|
||||
}
|
||||
|
||||
func GetHostname() string {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Error("GetHostname:", err)
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
func HasFlagName(fs *flag.FlagSet, s string) bool {
|
||||
var found bool
|
||||
fs.Visit(func(flag *flag.Flag) {
|
||||
if flag.Name == s {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
30
pkg/util/ip.go
Normal file
30
pkg/util/ip.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InternalIP return internal ip.
|
||||
func InternalIP() string {
|
||||
inters, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, inter := range inters {
|
||||
if inter.Flags&net.FlagUp != 0 && !strings.HasPrefix(inter.Name, "lo") {
|
||||
addrs, err := inter.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
18
pkg/util/time.go
Normal file
18
pkg/util/time.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package util
|
||||
|
||||
func CountDaysOfMonth(year int, month int) (days int) {
|
||||
if month != 2 {
|
||||
if month == 4 || month == 6 || month == 9 || month == 11 {
|
||||
days = 30
|
||||
} else {
|
||||
days = 31
|
||||
}
|
||||
} else {
|
||||
if ((year%4) == 0 && (year%100) != 0) || (year%400) == 0 {
|
||||
days = 29
|
||||
} else {
|
||||
days = 28
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
17
pkg/util/wait_group_wrapper.go
Normal file
17
pkg/util/wait_group_wrapper.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type WaitGroupWrapper struct {
|
||||
sync.WaitGroup
|
||||
}
|
||||
|
||||
func (w *WaitGroupWrapper) Wrap(cb func()) {
|
||||
w.Add(1)
|
||||
go func() {
|
||||
cb()
|
||||
w.Done()
|
||||
}()
|
||||
}
|
||||
12
pkg/version/ver.go
Normal file
12
pkg/version/ver.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var Binary string
|
||||
|
||||
func String(app string) string {
|
||||
return fmt.Sprintf("%s v%s (built w/%s)", app, Binary, runtime.Version())
|
||||
}
|
||||
Reference in New Issue
Block a user