├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── admire.jpg
├── app
├── jiacrontab_admin
│ ├── jiacrontab_admin.ini
│ └── main.go
└── jiacrontabd
│ ├── jiacrontabd.ini
│ └── main.go
├── deployment
├── jiacrontab_admin.service
├── jiacrontabctl
└── jiacrontabd.service
├── go.mod
├── go.sum
├── jiacrontab_admin
├── .gitignore
├── admin.go
├── app.go
├── config.go
├── const.go
├── crontab.go
├── ctx.go
├── daemon.go
├── debug.go
├── group.go
├── ldap.go
├── node.go
├── params.go
├── recover.go
├── runtime.go
├── srv.go
├── system.go
├── user.go
└── util.go
├── jiacrontabd
├── cmd.go
├── config.go
├── const.go
├── daemon.go
├── dependencies.go
├── jiacrontabd.go
├── job.go
├── srv.go
└── util.go
├── models
├── crontab.go
├── crontab_test.go
├── daemon.go
├── db.go
├── event.go
├── group.go
├── history.go
├── node.go
├── setting.go
└── user.go
├── pkg
├── base
│ ├── stat.go
│ └── storage.go
├── crontab
│ ├── crontab.go
│ ├── crontab_test.go
│ ├── job.go
│ ├── job_test.go
│ └── parse.go
├── file
│ └── file.go
├── finder
│ ├── finder.go
│ └── reader.go
├── kproc
│ ├── proc.go
│ ├── proc_posix.go
│ └── proc_windows.go
├── mailer
│ ├── login.go
│ └── mail.go
├── pprof
│ ├── pprof.go
│ ├── pprof_posix.go
│ └── pprof_windows.go
├── pqueue
│ ├── pqueue.go
│ └── pqueue_test.go
├── proto
│ ├── apicode.go
│ ├── args.go
│ ├── const.go
│ ├── crontab.go
│ ├── daemon.go
│ └── resp.go
├── rpc
│ ├── client.go
│ ├── client_test.go
│ ├── clients.go
│ └── server.go
├── test
│ ├── assertions.go
│ ├── fakes.go
│ └── logger.go
├── util
│ ├── arr.go
│ ├── fn.go
│ ├── ip.go
│ ├── time.go
│ └── wait_group_wrapper.go
└── version
│ └── ver.go
└── qq.png
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ---> Go
2 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
3 | *.o
4 | *.a
5 | *.so
6 |
7 | # Folders
8 | _obj
9 | _test
10 |
11 | # Architecture specific extensions/prefixes
12 | *.[568vq]
13 | [568vq].out
14 |
15 | *.cgo1.go
16 | *.cgo2.c
17 | _cgo_defun.c
18 | _cgo_gotypes.go
19 | _cgo_export.*
20 |
21 | _testmain.go
22 |
23 | *.exe
24 | *.test
25 | *.prof
26 | *.json
27 | client
28 | server
29 | .data
30 | data
31 | .idea
32 |
33 | .directory
34 | .vscode
35 |
36 | build
37 | binTmp
38 | dump.rdb
39 | app/jiacrontab_admin/jiacrontab_admin
40 | app/jiacrontabd/jiacrontabd
41 |
42 | app/jiacrontabd/logs
43 | app/jiacrontab_admin/logs
44 | app/jiacrontabd/pprof
45 | app/jiacrontab_admin/pprof
46 | /node_modules
47 |
48 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: go
3 | go:
4 | - 1.12.x
5 |
6 | # Only clone the most recent commit.
7 | git:
8 | depth: 1
9 |
10 | # Skip the install step. Don't `go get` dependencies. Only build with the code
11 | # in vendor/
12 | install: true
13 |
14 | matrix:
15 | fast_finish: true
16 | include:
17 | - go: 1.11.x
18 | env: GO111MODULE=on
19 | - go: 1.12.x
20 | env: GO111MODULE=on
21 |
22 | services:
23 | - postgresql
24 |
25 | # Don't email me the results of the test runs.
26 | notifications:
27 | email: false
28 |
29 | script:
30 | - make test
31 |
32 | after_success:
33 | - bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 更新记录
2 |
3 | ## v2.2.0
4 | 1. 支持钉钉通知
5 | 2. 修复mysql下无法自动创建表
6 | 3. 修复无法正确删除动态
7 | 4. 修复重启jiacrontabd时任务进程数量异常
8 | 5. 修复编辑定时任务时无法正确停止原有任务
9 |
10 | ## v2.1.0
11 | 1. 日志默认倒序查询
12 | 2. 任务支持根据关键词搜索
13 | 3. 新增磁盘清理动态清理
14 | 4. 节点日志管理
15 | 5. 修复bug
16 |
17 |
18 | ## v2.0.5
19 |
20 | 1. 修复修改job后前一次调度计划不能马上停止
21 | 2. 修复管理员审核时编辑普通用户job造成的普通用户job丢失
22 | 3. 修复密码无法修改
23 | 4. 新增不活跃节点列表
24 |
25 | ## v2.0.4
26 |
27 | 1. 修复常驻任务无法删除
28 | 2. 修复查看日志在特殊情况下日志异常
29 | 3. 新增修改分组
30 |
31 | ## v2.0.3
32 |
33 | 1. 修复特定情况下节点失去连接
34 | 2. 修复定时任务隔天无法生成日志目录
35 |
36 | ## v2.0.2
37 |
38 | 1. 修复由于多次修改job造成的定时器重复调度
39 |
40 | ## v2.0.1
41 |
42 | 1. 修复进程数量显示异常
43 | 2. 手动执行任务的支持kill
44 | 3. 修复死锁造成的运行异常
45 | 4. 修复依赖任务自定义代码不执行
46 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM yarnpkg/dev as frontend-env
2 |
3 | WORKDIR /jiacrontab
4 | RUN apt-get install git
5 | RUN git clone https://github.com/jiacrontab/jiacrontab-frontend.git
6 | WORKDIR /jiacrontab/jiacrontab-frontend
7 | RUN yarn && yarn build
8 |
9 | FROM golang AS jiacrontab-build
10 | WORKDIR /jiacrontab
11 | COPY . .
12 | COPY --from=frontend-env /jiacrontab/jiacrontab-frontend/build /jiacrontab/frontend-build
13 | RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct
14 | RUN GO111MODULE=on go install github.com/go-bindata/go-bindata/v3/go-bindata@latest
15 | RUN make build assets=frontend-build
16 |
17 | FROM debian AS jiarontab-run
18 | COPY --from=jiacrontab-build /jiacrontab/build /jiacrontab/build
19 | WORKDIR /jiacrontab/bin
20 | VOLUME ["/jiacrontab/bin/data"]
21 | EXPOSE 20001 20000 20003
22 | RUN mv /jiacrontab/build/jiacrontab/jiacrontabd/* . && mv /jiacrontab/build/jiacrontab/jiacrontab_admin/* .
23 | ENTRYPOINT []
24 |
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
5 |
6 | 1. Definitions.
7 |
8 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
9 |
10 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
11 |
12 | "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.
13 |
14 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
15 |
16 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
17 |
18 | "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.
19 |
20 | "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).
21 |
22 | "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.
23 |
24 | "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."
25 |
26 | "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.
27 |
28 | 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.
29 |
30 | 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.
31 |
32 | 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:
33 |
34 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
35 |
36 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
37 |
38 | (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
39 |
40 | (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.
41 |
42 | 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.
43 |
44 | 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.
45 |
46 | 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.
47 |
48 | 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.
49 |
50 | 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.
51 |
52 | 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.
53 |
54 | END OF TERMS AND CONDITIONS
55 |
56 | APPENDIX: How to apply the Apache License to your work.
57 |
58 | 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.
59 |
60 | Copyright [yyyy] [name of copyright owner]
61 |
62 | Licensed under the Apache License, Version 2.0 (the "License");
63 | you may not use this file except in compliance with the License.
64 | You may obtain a copy of the License at
65 |
66 | http://www.apache.org/licenses/LICENSE-2.0
67 |
68 | Unless required by applicable law or agreed to in writing, software
69 | distributed under the License is distributed on an "AS IS" BASIS,
70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
71 | See the License for the specific language governing permissions and
72 | limitations under the License.
73 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Go parameters
2 | goCmd=go
3 | version=$(shell cat VERSION)
4 | goBuild=$(goCmd) build -ldflags "-X jiacrontab/pkg/version.Binary=$(version)"
5 | goClean=$(goCmd) clean
6 | goTest=$(goCmd) test
7 | goGet=$(goCmd) get
8 | sourceAdmDir=./app/jiacrontab_admin
9 | sourceNodeDir=./app/jiacrontabd
10 | binAdm=$(sourceAdmDir)/jiacrontab_admin
11 | binNode=$(sourceNodeDir)/jiacrontabd
12 |
13 |
14 | buildDir=./build
15 | buildAdmDir=$(buildDir)/jiacrontab/jiacrontab_admin
16 | buildNodeDir=$(buildDir)/jiacrontab/jiacrontabd
17 |
18 | admCfg=$(sourceAdmDir)/jiacrontab_admin.ini
19 | nodeCfg=$(sourceNodeDir)/jiacrontabd.ini
20 | staticDir=./jiacrontab_admin/static/build
21 | staticSourceDir=./jiacrontab_admin/static
22 | workDir=$(shell pwd)
23 |
24 |
25 | .PHONY: all build test clean build-linux build-windows
26 | all: test build
27 | build:
28 | $(call checkStatic)
29 | $(call init)
30 | $(goBuild) -o $(binAdm) -v $(sourceAdmDir)
31 | $(goBuild) -o $(binNode) -v $(sourceNodeDir)
32 | mv $(binAdm) $(buildAdmDir)
33 | mv $(binNode) $(buildNodeDir)
34 | build2:
35 | $(call init)
36 | $(goBuild) -o $(binAdm) -v $(sourceAdmDir)
37 | $(goBuild) -o $(binNode) -v $(sourceNodeDir)
38 | mv $(binAdm) $(buildAdmDir)
39 | mv $(binNode) $(buildNodeDir)
40 | docker:
41 | docker build \
42 | -t iwannay/jiacrontab:$(version) \
43 | -f Dockerfile \
44 | .
45 | test:
46 | $(goTest) -v -race -coverprofile=coverage.txt -covermode=atomic $(sourceAdmDir)
47 | $(goTest) -v -race -coverprofile=coverage.txt -covermode=atomic $(sourceNodeDir)
48 | clean:
49 | rm -f $(binAdm)
50 | rm -f $(binNode)
51 | rm -rf $(buildDir)
52 |
53 |
54 | # Cross compilation
55 | build-linux:
56 | $(call checkStatic)
57 | $(call init)
58 | GOOS=linux GOARCH=amd64 $(goBuild) -o $(binAdm) -v $(sourceAdmDir)
59 | GOOS=linux GOARCH=amd64 $(goBuild) -o $(binNode) -v $(sourceNodeDir)
60 | mv $(binAdm) $(buildAdmDir)
61 | mv $(binNode) $(buildNodeDir)
62 |
63 | build-windows:
64 | $(call checkStatic)
65 | $(call init)
66 | 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)
67 | 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)
68 |
69 | mv $(binAdm).exe $(buildAdmDir)
70 | mv $(binNode).exe $(buildNodeDir)
71 |
72 | define checkStatic
73 | @if [ "$(assets)" = "" ]; then echo "no assets, see https://github.com/jiacrontab/jiacrontab-frontend"; exit -1;else echo "build release"; fi
74 | go-bindata -pkg admin -prefix $(assets) -o jiacrontab_admin/bindata_gzip.go -fs $(assets)/...
75 | endef
76 |
77 | define init
78 | rm -rf $(buildDir)
79 | mkdir $(buildDir)
80 | mkdir -p $(buildAdmDir)
81 | mkdir -p $(buildNodeDir)
82 | cp $(admCfg) $(buildAdmDir)
83 | cp $(nodeCfg) $(buildNodeDir)
84 | endef
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## jiacrontab
2 |
3 | [](https://travis-ci.org/iwannay/jiacrontab)
4 |
5 | 简单可信赖的任务管理工具
6 |
7 | **如果你需要一次性推送作业到数以万计的实例上运行,请关注作者开源的另一款调度软件 [Jiascheduler](https://github.com/jiawesoft/jiascheduler),jiascheduler更强大,更灵活,并具备jiacrontab的全部功能**
8 |
9 | ### v2.0.0版发布
10 |
11 |
12 | ### [❤jiacrontab 最新版下载点这里❤ ](https://download.iwannay.cn/jiacrontab/)
13 |
14 | 1.自定义job执行
15 | 2.允许设置job的最大并发数
16 | 3.每个脚本都可在web界面下灵活配置,如测试脚本运行,查看日志,强杀进程,停止定时...
17 | 4.允许添加脚本依赖(支持跨服务器),依赖脚本提供同步和异步的执行模式
18 | 5.支持异常通知
19 | 6.支持守护脚本进程
20 | 7.支持节点分组
21 |
22 |
23 | ### 架构
24 |
25 |
26 |
27 | ### 说明
28 |
29 | jiacrontab 由 jiacrontab_admin,jiacrontabd 两部分构成,两者完全独立通过 rpc 通信
30 | jiacrontab_admin:管理后台向用户提供web操作界面
31 | jiacrontabd:负责job数据存储,任务调度
32 |
33 |
34 | ### 安装
35 |
36 | #### 二进制安装
37 |
38 | 1.[下载](https://download.iwannay.cn/jiacrontab/) 二进制文件。
39 |
40 | 2.解压缩进入目录(jiarontab_admin,jiacrontabd)。
41 |
42 | 3.运行
43 |
44 | ```sh
45 | $ nohup ./jiacrontab_admin &> jiacrontab_admin.log &
46 | $ nohup ./jiacrontabd &> jiacrontabd.log &
47 |
48 | ## 建议使用systemd守护
49 | ```
50 |
51 | #### v2.x.x源码安装
52 |
53 | 1.安装 git,golang(version 1.12.x);可参考官网。
54 | 2.安装运行
55 |
56 | ```sh
57 | $ git clone git@github.com:iwannay/jiacrontab.git
58 | $ cd jiacrontab
59 | # 配置代理
60 | $ go env -w GONOPROXY=\*\*.baidu.com\*\* ## 配置GONOPROXY环境变量,所有百度内代码,不走代理
61 | $ go env -w GONOSUMDB=\* ## 配置GONOSUMDB,暂不支持sumdb索引
62 | $ go env -w GOPROXY=https://goproxy.baidu.com ## 配置GOPROXY,可以下载墙外代码
63 |
64 | # 编译
65 | # 注意需要先编译前端(https://github.com/jiacrontab/jiacrontab-frontend)
66 | # 再安装go-bindata
67 | # 然后assets指定前端资源编译后的位置
68 | $ GO111MODULE=on go get -u github.com/go-bindata/go-bindata/v3/go-bindata
69 | $ make build assets=$HOME/project/jiacrontab-frontend/build
70 |
71 | $ cd build/jiacrontab/jiacrontab_admin/
72 | $ nohup ./jiacrontab_admin &> jiacrontab_admin.log &
73 |
74 | $ cd build/jiacrontab/jiacrontabd/
75 | $ nohup ./jiacrontabd &> jiacrontabd.log &
76 | ```
77 |
78 | 浏览器访问 host:port (eg: localhost:20000) 即可访问管理后台
79 |
80 | #### docker 安装
81 | ```sh
82 | # 下载镜像
83 | $ docker pull iwannay/jiacrontab:2.3.0
84 |
85 | # 创建自定义网络
86 | $ docker network create mybridge
87 |
88 | # 启动jiacrontab_admin
89 | # 需要指定配置文件目录时需要先挂载目录,然后-config指定
90 | $ docker run --network mybridge --name jiacrontab_admin -p 20000:20000 -it iwannay/jiacrontab:2.3.0 ./jiacrontab_admin
91 |
92 | # 启动jiacrontabd
93 | # 需要指定配置文件目录时需要先挂载目录,然后-config指定
94 | $ docker run -v $(pwd)/jiacrontabd:/config --name jiacrontabd --network mybridge -it iwannay/jiacrontab:2.3.0 ./jiacrontabd -config /config/jiacrontabd.ini
95 |
96 | ```
97 |
98 | ### 升级版本
99 |
100 | 1、下载新版本压缩包,并解压。
101 |
102 | 2、替换旧版jiacrontab_admin,jiacrontabd为新版执行文件
103 |
104 | 3、运行
105 |
106 | ### 基本使用
107 |
108 | #### 定时任务
109 |
110 | ##### 超时设置和超时操作
111 |
112 | 超时后会进行设置的超时操作 默认值为 0 不判断超时
113 |
114 | ##### 最大并发数
115 |
116 | 最大并发数控制同一job同一个时刻最多允许存在的进程数,默认最大并发数为1,当前一次未执行结束时则放弃后续执行。
117 | 防止脚本无法正常退出而导致系统资源耗尽
118 |
119 | ##### 添加依赖
120 |
121 | 依赖就是用户脚本执行前,需要先执行依赖脚本,只有依赖脚本执行完毕才会执行当前脚本。
122 | 1. **并发执行**
123 | 并发执行依赖脚本,任意一个脚本出错或超时不会影响其他依赖脚本,但是会中断用户job
124 | 2. **同步执行**
125 | 同步执行依赖脚本,执行顺序为添加顺序,如果有一个依赖脚本出错或超时,则会中断后继依赖,以及用户job
126 |
127 | ##### 脚本异常退出通知
128 |
129 | 如果脚本退出码不为0,则认为是异常退出
130 |
131 | #### 常驻任务
132 |
133 | 常驻任务检查脚本进程是否退出,如果退出再次重启,保证脚本不停运行。
134 | 注意:不支持后台进程。
135 |
136 | ## 附录
137 |
138 | ### 错误日志
139 |
140 | 错误日志存放在配置文件设置的目录下
141 | 定时任务为 logs/crontab_task
142 | 定时任务为 logs/daemon_task
143 | 日志文件准确为日期目录下的 ID.log (eg: logs/crontab_task/2018/01/01/1.log)
144 |
145 | #### 错误日志信息
146 |
147 | 1. 正常错误日志
148 | 程序原因产生的错误日志
149 | 2. 自定义错误日志
150 | 程序中自定义输出的信息,需要在输出信息后面加入换行
151 |
152 | ### 截图
153 | 
154 |
155 | ### 演示地址
156 |
157 | [2.0.0版本演示地址](http://jiacrontab-spa.iwannay.cn/) 账号:test 密码:123456
158 |
159 |
160 |
161 | ### 赞助
162 | 本项目花费了作者大量时间,如果你觉的该项目对你有用,或者你希望该项目有更好的发展,欢迎赞助。
163 | 
164 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 2.3.0
--------------------------------------------------------------------------------
/admire.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iwannay/jiacrontab/556339ada56bf88d00796c3291b5e71bb547abd2/admire.jpg
--------------------------------------------------------------------------------
/app/jiacrontab_admin/jiacrontab_admin.ini:
--------------------------------------------------------------------------------
1 | [app]
2 | http_listen_addr = 0.0.0.0:20000
3 | rpc_listen_addr = :20003
4 | app_name = jiacrontab
5 | ; http 返回签名
6 | signing_key = `WERRTT1234$@#@@$`
7 | log_level = warn
8 | ; 客户端最大心跳时间
9 | max_client_alive_interval = 30
10 |
11 | [jwt]
12 | ; jwt 签名
13 | signing_key = eyJhbGciOiJIUzI1
14 | expires = 3600
15 | name = token
16 |
17 | [mail]
18 | enabled = true
19 | host = smtp.163.com:25
20 | user = jiacrontab@163.com
21 | skip_verify = true
22 | passwd = xxxxxx
23 | from = jiacrontab@163.com
24 | use_certificate = false
25 |
26 | [ldap]
27 | ; 支持: ldap://, ldaps://, ldapi://.
28 | addr = ladp://localhost:1234
29 | disabled_anonymous_query = false
30 | bind_passwd= 123456
31 | bind_userdn = "cn=admin,dc=jdevops,dc=com"
32 | basedn = "dc=jdevops,dc=com"
33 | user_field = uid
34 |
35 | [database]
36 | ; jiacrontab_admin目前支持的数据库包括sqlite3,mysql,pg
37 | ; 注意: mysql,pg 等数据库需要手动建立jiacrontab库
38 | ; driver_name = postgres
39 | ; dsn = postgres://jiacrontab:123456@localhost:5432/jiacrontab?sslmode=disable
40 | ; driver_name = mysql
41 | ; dsn = root:12345678@(localhost:3306)/jiacrontab?charset=utf8&parseTime=True&loc=Local
42 | driver_name = sqlite3
43 | dsn = data/jiacrontab_admin.db?cache=shared
44 |
45 |
--------------------------------------------------------------------------------
/app/jiacrontab_admin/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | admin "jiacrontab/jiacrontab_admin"
6 | "jiacrontab/pkg/pprof"
7 | "os"
8 |
9 | "flag"
10 |
11 | "jiacrontab/pkg/version"
12 |
13 | "jiacrontab/pkg/util"
14 |
15 | "github.com/iwannay/log"
16 | )
17 |
18 | var (
19 | debug bool
20 | cfgPath string
21 | logLevel string
22 | user string
23 | resetpwd bool
24 | pwd string
25 | )
26 |
27 | func parseFlag(opt *admin.Config) *flag.FlagSet {
28 | flagSet := flag.NewFlagSet("jiacrontab_admin", flag.ExitOnError)
29 | // app options
30 | flagSet.Bool("version", false, "打印版本信息")
31 | flagSet.Bool("help", false, "帮助信息")
32 | flagSet.StringVar(&logLevel, "log_level", "warn", "日志级别(debug|info|warn|error)")
33 | flagSet.BoolVar(&debug, "debug", false, "开启debug模式")
34 | flagSet.StringVar(&cfgPath, "config", "./jiacrontab_admin.ini", "配置文件路径")
35 | flagSet.BoolVar(&resetpwd, "resetpwd", false, "重置密码")
36 | flagSet.StringVar(&pwd, "pwd", "", "重置密码时的新密码")
37 | flagSet.StringVar(&user, "user", "", "重置密码时的用户名")
38 | // jwt options
39 | flagSet.Parse(os.Args[1:])
40 |
41 | if flagSet.Lookup("version").Value.(flag.Getter).Get().(bool) {
42 | fmt.Println(version.String("jiacrontab_admin"))
43 | os.Exit(0)
44 | }
45 | if flagSet.Lookup("help").Value.(flag.Getter).Get().(bool) {
46 | flagSet.Usage()
47 | os.Exit(0)
48 | }
49 |
50 | opt.CfgPath = cfgPath
51 |
52 | opt.Resolve()
53 |
54 | if util.HasFlagName(flagSet, "debug") {
55 | opt.App.Debug = debug
56 | }
57 |
58 | if util.HasFlagName(flagSet, "log_level") {
59 | opt.App.LogLevel = logLevel
60 | }
61 | if debug {
62 | log.JSON("debug config:", opt)
63 | }
64 | return flagSet
65 | }
66 |
67 | func main() {
68 | cfg := admin.NewConfig()
69 | parseFlag(cfg)
70 | log.SetLevel(map[string]int{
71 | "debug": 0,
72 | "info": 1,
73 | "warn": 2,
74 | "error": 3,
75 | }[cfg.App.LogLevel])
76 | pprof.ListenPprof()
77 | admin := admin.New(cfg)
78 | if resetpwd {
79 | if err := admin.ResetPwd(user, pwd); err != nil {
80 | fmt.Printf("failed reset passwrod (%s)\n", err)
81 | } else {
82 | fmt.Printf("reset password success!\n")
83 | }
84 | os.Exit(0)
85 |
86 | }
87 | admin.Main()
88 | }
89 |
--------------------------------------------------------------------------------
/app/jiacrontabd/jiacrontabd.ini:
--------------------------------------------------------------------------------
1 | [jiacrontabd]
2 | ; 任务日志页面显示冗余信息,比如时间、脚本名称
3 | verbose_job_log = false
4 | ; 本机rpc监听地址
5 | listen_addr = :20002
6 | ; 当前节点的广播地址,admin通过该地址与当前节点通信,默认取当前节点ip
7 | ; boardcast_addr = localhost:20001
8 | ; admin 地址
9 | admin_addr = jiacrontab_admin:20003
10 | ; 自动清理大于一个月或者单文件体积大于1G的日志文件
11 | auto_clean_task_log = true
12 | ; 节点名,默认取节点hostname
13 | ; node_name = node1
14 | log_level = warn
15 | log_path = ./logs
16 | user_agent = jiacrontabd
17 | ; jiacrontabd目前仅支持sqlite3
18 | driver_name = sqlite3
19 | dsn = data/jiacrontabd.db?cache=shared
20 |
21 | ; 心跳上报周期(s)
22 | client_alive_interval = 10
23 |
--------------------------------------------------------------------------------
/app/jiacrontabd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "jiacrontab/jiacrontabd"
7 | "jiacrontab/pkg/pprof"
8 | "jiacrontab/pkg/util"
9 | "jiacrontab/pkg/version"
10 |
11 | "os"
12 |
13 | "github.com/iwannay/log"
14 | )
15 |
16 | func parseFlag(opt *jiacrontabd.Config) *flag.FlagSet {
17 |
18 | var (
19 | debug bool
20 | cfgPath string
21 | logLevel string
22 | boardcastAddr string
23 | )
24 |
25 | flagSet := flag.NewFlagSet("jiacrontabd", flag.ExitOnError)
26 | flagSet.Bool("version", false, "打印版本信息")
27 | flagSet.Bool("help", false, "帮助信息")
28 | flagSet.StringVar(&logLevel, "log_level", "warn", "日志级别(debug|info|warn|error)")
29 | flagSet.BoolVar(&debug, "debug", false, "开启debug模式")
30 | flagSet.StringVar(&boardcastAddr, "boardcast_addr", "", fmt.Sprintf("广播地址(default: %s:20001)", util.InternalIP()))
31 | flagSet.StringVar(&cfgPath, "config", "./jiacrontabd.ini", "配置文件路径")
32 | flagSet.Parse(os.Args[1:])
33 | if flagSet.Lookup("version").Value.(flag.Getter).Get().(bool) {
34 | fmt.Println(version.String("jiacrontab_admin"))
35 | os.Exit(0)
36 | }
37 | if flagSet.Lookup("help").Value.(flag.Getter).Get().(bool) {
38 | flagSet.Usage()
39 | os.Exit(0)
40 | }
41 |
42 | opt.CfgPath = cfgPath
43 | opt.Resolve()
44 |
45 | // TODO: can be better
46 | if util.HasFlagName(flagSet, "log_level") {
47 | opt.LogLevel = logLevel
48 | }
49 |
50 | if util.HasFlagName(flagSet, "debug") {
51 | opt.Debug = debug
52 | }
53 |
54 | if util.HasFlagName(flagSet, "boardcast_addr") {
55 | opt.BoardcastAddr = boardcastAddr
56 | }
57 |
58 | if debug {
59 | log.JSON("debug config:", opt)
60 | }
61 |
62 | return flagSet
63 | }
64 |
65 | func main() {
66 | cfg := jiacrontabd.NewConfig()
67 | parseFlag(cfg)
68 | log.SetLevel(map[string]int{
69 | "debug": 0,
70 | "info": 1,
71 | "warn": 2,
72 | "error": 3,
73 | }[cfg.LogLevel])
74 | pprof.ListenPprof()
75 | jiad := jiacrontabd.New(cfg)
76 | jiad.Main()
77 | }
78 |
--------------------------------------------------------------------------------
/deployment/jiacrontab_admin.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=jiacrontab_admin service
3 | After=network.target
4 |
5 | [Install]
6 | WantedBy=multi-user.target
7 |
8 |
9 | [Service]
10 | Type=simple
11 | User=root
12 | Group=root
13 | ProtectSystem=full
14 | WorkingDirectory=/opt/jiacrontab/jiacrontab_admin
15 | ExecStart=/opt/jiacrontab/jiacrontab_admin/jiacrontab_admin
16 | KillMode=process
17 | KillSignal=SIGTERM
18 | SendSIGKILL=no
19 | Restart=on-abort
20 | RestartSec=5s
21 | UMask=007
--------------------------------------------------------------------------------
/deployment/jiacrontabctl:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # -----------------------------------------------------------------
3 | # The initial version was released by Vipkwd
4 | # -----------------------------------------------------------------
5 | # 应用场景:本脚本需配合jiacrontab v2.2.0+ 版本使用
6 | # (https://jiacrontab.iwannay.cn/download/jiacrontab-v2.2.0-linux-amd64.zip)
7 | #
8 | # 部署方法:1、解压上述zip二进制包; 2、将本脚本放入解压后的目录终呈如下结构
9 | #
10 | # [root@vipkwd jiacrontab]# pwd
11 | # /data/wwwroot/jiacrontab
12 | # [root@vipkwd jiacrontab]# ll
13 | # 总用量 16
14 | # 1033992 -rwx--x--x 1 root root 7.4K 8月 14 21:30 jiacrontabctl
15 | # 1034047 drwxr-xr-x 3 root www 4.0K 8月 11 08:27 jiacrontab_admin
16 | # 1033995 drwxr-xr-x 4 root www 4.0K 8月 11 14:24 jiacrontabd
17 | # [root@vipkwd_com jiacrontab]#
18 | # -----------------------------------------------------------------
19 |
20 |
21 | # -----配置项 start-----
22 | # 提供进程搜索关键字(即: "jiacrontab_admin"与“jiacrontabd” 在公共字符串部分,用于shell grep 过滤)
23 | APP_KEYWORDS=jiacrontab
24 |
25 | # 提供web前端服务的脚本文件(目录)名(对应: ./jiacrontab_admin/jiacrontab_admin)
26 | ADMIN_SCRIPT_NAME=jiacrontab_admin
27 |
28 | # 任务调度服务脚本文件(目录)名(对应: ./jiacrontabd/jiacrontabd)
29 | BD_SCRIPT_NAME=jiacrontabd
30 |
31 | #手动指定项目PATH(默认空,自动定位 pwd 指令目录)
32 | DEPLOY_PATH=
33 | # -----配置项 end-----
34 |
35 |
36 | FORMATER_LINE="------------------------------------------------------------------------------------------------------"
37 | ECHO_PREFIX=" -- "
38 | STIME_LOGFILE=".stime.log"
39 |
40 | # 绝对定位工作目录
41 | PROJECT_ROOT=${DEPLOY_PATH:=`pwd`}
42 | if [ ! -d $PROJECT_ROOT ];then
43 | echo -e "[\033[31mError\033[0m] DEPLOY_PATH \033[33m${DEPLOY_PATH}\033[0m is not a directory!"
44 | exit 1
45 | fi
46 | ai=0
47 | apps=("${ADMIN_SCRIPT_NAME}" "${BD_SCRIPT_NAME}")
48 | for app in ${apps[*]}
49 | do
50 | path=${PROJECT_ROOT}/${app}
51 | if [ ! -d $path ];then
52 | echo -e "[\033[31mError\033[0m] Project path: \033[33m${path}\033[0m is not a directory!"
53 | ai=$(($ai+1))
54 | fi
55 | done
56 |
57 | apps=""
58 |
59 | [[ $ai -gt 0 ]] && exit 1
60 |
61 | ADMIN_CLI_IS_RUNNING=0
62 | BD_CLI_IS_RUNNING=0
63 |
64 | function _timeNow(){
65 | nowtime=`date "+%Y-%m-%d %H:%M:%S"`
66 | }
67 |
68 | function _runApp(){
69 | if [ "${1}" == "0" ];then
70 | path=${PROJECT_ROOT}/${2}
71 | if [ ! -d $path ];then
72 | echo -e "[\033[31mError\033[0m] \033[33m${path}\033[0m is not directory(Skiped)!"
73 | return 0
74 | fi
75 |
76 | if [ ! -f "${path}/${2}" ];then
77 | echo -e "[\033[31mError\033[0m] Service script \033[33m${path}/${2}\033[0m is not exist(Skiped)!"
78 | return 0
79 | fi
80 |
81 | cd $path
82 | `nohup ./${2} &> ${2}.log &`
83 | #sleep 1;
84 | _timeNow
85 | echo "${2}[ StartTime: ${nowtime} ]" >> ${PROJECT_ROOT}/${STIME_LOGFILE}
86 | echo -e " ${ECHO_PREFIX} \033[33m${2}\033[0m (\033[32mStarted\033[0m)"
87 | else
88 | echo -e " ${ECHO_PREFIX} \033[33m${2:=Unknow}\033[0m is running (\033[33mSkiped\033[0m)"
89 | fi
90 | }
91 |
92 |
93 | function _cliListen(){
94 | for cli in `ps -ef | grep "$APP_KEYWORDS" | grep -v "grep" | awk -F ' ./' '{print $2}'`
95 | do
96 | if [ "${cli}" == "${ADMIN_SCRIPT_NAME}" ];then
97 | ADMIN_CLI_IS_RUNNING=1
98 | fi
99 | if [ "${cli}" == "${BD_SCRIPT_NAME}" ];then
100 | BD_CLI_IS_RUNNING=1
101 | fi
102 | done
103 | }
104 |
105 | function _parseDuringTime(){
106 | #parseTimeResultStr=""
107 | logfile="${PROJECT_ROOT}/${STIME_LOGFILE}"
108 | if [ -f $logfile ];then
109 | stime=`grep "${1}\[ StartTime:" ${logfile} | tail -1 | awk '{print $3 " "$4}'`
110 | _timeNow
111 | time1=$(($(date +%s -d "$nowtime") - $(date +%s -d "$stime")));
112 | array=("Duration: ")
113 | config=(31579200 "Year" 2635200 "Month" 86400 "Day" 3600 "Hour" 60 "Minute" 1 "Second")
114 | j=0
115 | for str in ${config[*]}; do
116 | if [ $(($j%2)) -eq 0 ];then
117 | if [ ${time1} -ge ${config[$j]} ];then
118 | j1=$(($j+1))
119 | j2=$(($j+2))
120 | array[$j1]=$[ $time1 / ${config[$j]} ]
121 | array[$j2]=${config[$j1]}
122 | if [ ${array[$j1]} -gt 1 ];then
123 | array[$j2]="${array[$j2]}s"
124 | fi
125 | time1=$(($time1 % ${config[$j]}))
126 | fi
127 | fi
128 | j=$(($j+1))
129 | done
130 | if [ "Null" == "${stime}Null" ];then
131 | return 0
132 | fi
133 | parseTimeResultStr="${stime} (${array[*]})"
134 | return 1
135 | fi
136 | parseTimeResultStr=""
137 | return 0
138 | }
139 |
140 | function start(){
141 | _cliListen
142 | if [ $ADMIN_CLI_IS_RUNNING -eq 0 -a $BD_CLI_IS_RUNNING -eq 0 ];then
143 | echo "" > ${PROJECT_ROOT}/${STIME_LOGFILE}
144 | fi
145 | _runApp "${ADMIN_CLI_IS_RUNNING}" "${ADMIN_SCRIPT_NAME}"
146 | _runApp "${BD_CLI_IS_RUNNING}" "${BD_SCRIPT_NAME}"
147 | }
148 |
149 | function stop(){
150 | for line in `ps -ef | grep "$APP_KEYWORDS" | grep -v grep | awk -F ' ' '{print $2 " " $NF}'`
151 | do
152 | STR=`echo $line | awk '{print $1}'`
153 | if [ $STR -gt 0 ] 2> /dev/null ;then
154 | kill -9 $STR;
155 | else
156 | echo -e " ${ECHO_PREFIX} \033[33m${STR}\033[0m (\033[34mStoped\033[0m) "
157 | #sleep 1;
158 | fi
159 | done
160 | echo "" > ${PROJECT_ROOT}/${STIME_LOGFILE}
161 | }
162 |
163 | function status(){
164 | _cliListen
165 | echo $FORMATER_LINE
166 | echo " ${ECHO_PREFIX}"
167 | if [ $ADMIN_CLI_IS_RUNNING -eq 0 ];then
168 | echo -e " ${ECHO_PREFIX} Active: \033[31minactive (dead)\033[0m \033[33m${ADMIN_SCRIPT_NAME}\033[0m since Unknow"
169 | else
170 | _parseDuringTime "${ADMIN_SCRIPT_NAME}"
171 | if [ $? -gt 0 ] 2> /dev/null;then
172 | # 全局获取函数返回的字符串
173 | echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${ADMIN_SCRIPT_NAME}\033[0m since ${parseTimeResultStr}"
174 | #sleep 1
175 | else
176 | echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${ADMIN_SCRIPT_NAME}\033[0m since Unknow"
177 | fi
178 | fi
179 |
180 | if [ $BD_CLI_IS_RUNNING -eq 0 ];then
181 | echo -e " ${ECHO_PREFIX} Active: \033[31minactive (dead)\033[0m \033[33m${BD_SCRIPT_NAME}\033[0m since Unknow"
182 | else
183 | _parseDuringTime "${BD_SCRIPT_NAME}"
184 | if [ $? -gt 0 ] 2> /dev/null;then
185 | # 全局获取函数返回的字符串
186 | echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${BD_SCRIPT_NAME}\033[0m since ${parseTimeResultStr}"
187 | #sleep 1
188 | else
189 | echo -e " ${ECHO_PREFIX} Active: \033[32mactive (running)\033[0m \033[33m${BD_SCRIPT_NAME}\033[0m since Unknow"
190 | fi
191 | fi
192 | echo " ${ECHO_PREFIX}"
193 | echo $FORMATER_LINE
194 | if [ $ADMIN_CLI_IS_RUNNING -gt 0 -o $BD_CLI_IS_RUNNING -gt 0 ];then
195 |
196 | ps -ef | grep "$APP_KEYWORDS" | grep -v grep
197 | sleep 1
198 | echo $FORMATER_LINE
199 | netstat -tlpan | grep "$APP_KEYWORDS" | grep -v grep
200 | fi
201 | }
202 |
203 | function cli(){
204 | echo "input: $0 OPTION" >&2
205 | echo " OPTION :"
206 | echo " start"
207 | echo " stop"
208 | echo " status"
209 | echo " restart"
210 | echo " h | help"
211 | }
212 |
213 | function helpDoc() {
214 | echo $FORMATER_LINE
215 | #echo " "
216 | echo " Jiacrontab 简单可信赖的任务管理工具(V2.2.0)"
217 | echo " 1.自定义job执行"
218 | echo " 2.允许设置job的最大并发数"
219 | echo " 3.每个脚本都可在web界面下灵活配置,如测试脚本运行,查看日志,强杀进程,停止定时..."
220 | echo " 4.允许添加脚本依赖(支持跨服务器),依赖脚本提供同步和异步的执行模式"
221 | echo " 5.支持异常通知"
222 | echo " 6.支持守护脚本进程"
223 | echo " 7.支持节点分组"
224 | echo " n.更多请访问仓库查看"
225 | #echo " "
226 | echo $FORMATER_LINE
227 | #echo " "
228 | echo " Github: https://github.com/iwannay/jiacrontab"
229 | echo " Csdn: https://codechina.csdn.net/mirrors/iwannay/jiacrontab"
230 | echo " "
231 | echo " Package: https://jiacrontab.iwannay.cn/download/"
232 | #echo " "
233 | echo $FORMATER_LINE
234 | cli
235 | }
236 | COMMAND=$1
237 | shift 1
238 | case $COMMAND in
239 | start)
240 | start;
241 | ;;
242 | stop)
243 | stop;
244 | ;;
245 | status)
246 | status;
247 | ;;
248 | restart)
249 | stop;
250 | sleep 1
251 | start;
252 | sleep 1
253 | status;
254 | ;;
255 | h|help)
256 | helpDoc;
257 | ;;
258 | *)
259 | cli;
260 | exit 1;
261 | ;;
262 | esac
--------------------------------------------------------------------------------
/deployment/jiacrontabd.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=jiacrontabd service
3 | After=network.target
4 |
5 | [Install]
6 | WantedBy=multi-user.target
7 |
8 | [Service]
9 | Type=simple
10 | User=root
11 | Group=root
12 | ProtectSystem=full
13 | WorkingDirectory=/opt/jiacrontab/jiacrontabd
14 | ExecStart=/opt/jiacrontab/jiacrontabd/jiacrontabd
15 | KillMode=process
16 | KillSignal=SIGTERM
17 | SendSIGKILL=no
18 | Restart=on-abort
19 | RestartSec=5s
20 | UMask=007
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module jiacrontab
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/Joker/hpp v1.0.0 // indirect
7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
8 | github.com/go-ldap/ldap/v3 v3.3.0
9 | github.com/gofrs/uuid v3.2.0+incompatible
10 | github.com/google/go-cmp v0.5.1 // indirect
11 | github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect
12 | github.com/iris-contrib/middleware/cors v0.0.0-20200810001613-32cf668f999f
13 | github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f
14 | github.com/iwannay/log v0.0.0-20190630100042-7fa98f256ca1
15 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
16 | github.com/kataras/iris/v12 v12.1.9-0.20200814111841-d0d7679a98f2
17 | github.com/lib/pq v1.8.0 // indirect
18 | github.com/mattn/go-colorable v0.1.7 // indirect
19 | github.com/mattn/go-sqlite3 v1.14.4 // indirect
20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
21 | github.com/modern-go/reflect2 v1.0.1 // indirect
22 | github.com/onsi/ginkgo v1.10.1 // indirect
23 | github.com/onsi/gomega v1.7.0 // indirect
24 | github.com/smartystreets/assertions v1.0.1 // indirect
25 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
26 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
27 | github.com/yudai/pp v2.0.1+incompatible // indirect
28 | golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
29 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
30 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
31 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
32 | gopkg.in/ini.v1 v1.58.0
33 | gorm.io/driver/mysql v1.0.3
34 | gorm.io/driver/postgres v1.0.5
35 | gorm.io/driver/sqlite v1.1.3
36 | gorm.io/gorm v1.20.6
37 | )
38 |
--------------------------------------------------------------------------------
/jiacrontab_admin/.gitignore:
--------------------------------------------------------------------------------
1 | bindata_gzip.go
--------------------------------------------------------------------------------
/jiacrontab_admin/admin.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "jiacrontab/models"
6 | "jiacrontab/pkg/mailer"
7 | "jiacrontab/pkg/rpc"
8 | "time"
9 |
10 | "sync/atomic"
11 |
12 | "github.com/kataras/iris/v12"
13 | )
14 |
15 | type Admin struct {
16 | cfg atomic.Value
17 | ldap *Ldap
18 | initAdminUser int32
19 | }
20 |
21 | func (n *Admin) getOpts() *Config {
22 | return n.cfg.Load().(*Config)
23 | }
24 |
25 | func (n *Admin) swapOpts(opts *Config) {
26 | n.cfg.Store(opts)
27 | }
28 |
29 | func New(opt *Config) *Admin {
30 | adm := &Admin{}
31 | adm.swapOpts(opt)
32 | return adm
33 | }
34 |
35 | func (a *Admin) init() {
36 | cfg := a.getOpts()
37 | if err := models.InitModel(cfg.Database.DriverName, cfg.Database.DSN, cfg.App.Debug); err != nil {
38 | panic(err)
39 | }
40 | if models.DB().Take(&models.User{}, "group_id=?", 1).Error == nil {
41 | atomic.StoreInt32(&a.initAdminUser, 1)
42 | }
43 | // mail
44 | if cfg.Mailer.Enabled {
45 | mailer.InitMailer(&mailer.Mailer{
46 | QueueLength: cfg.Mailer.QueueLength,
47 | SubjectPrefix: cfg.Mailer.SubjectPrefix,
48 | From: cfg.Mailer.From,
49 | Host: cfg.Mailer.Host,
50 | User: cfg.Mailer.User,
51 | Passwd: cfg.Mailer.Passwd,
52 | FromEmail: cfg.Mailer.FromEmail,
53 | DisableHelo: cfg.Mailer.DisableHelo,
54 | HeloHostname: cfg.Mailer.HeloHostname,
55 | SkipVerify: cfg.Mailer.SkipVerify,
56 | UseCertificate: cfg.Mailer.UseCertificate,
57 | CertFile: cfg.Mailer.CertFile,
58 | KeyFile: cfg.Mailer.KeyFile,
59 | UsePlainText: cfg.Mailer.UsePlainText,
60 | HookMode: false,
61 | })
62 | }
63 | a.ldap = &Ldap{
64 | BindUserDn: cfg.Ldap.BindUserdn,
65 | BindPwd: cfg.Ldap.BindPasswd,
66 | BaseOn: cfg.Ldap.Basedn,
67 | UserField: cfg.Ldap.UserField,
68 | Addr: cfg.Ldap.Addr,
69 | DisabledAnonymousQuery: cfg.Ldap.DisabledAnonymousQuery,
70 | Timeout: time.Second * time.Duration(cfg.Ldap.Timeout),
71 | }
72 | }
73 |
74 | func (a *Admin) ResetPwd(username string, password string) error {
75 | if username == "" || password == "" {
76 | return errors.New("username or password cannot empty")
77 | }
78 | a.init()
79 | user := models.User{
80 | Username: username,
81 | Passwd: password,
82 | }
83 | return user.Update()
84 | }
85 |
86 | func (a *Admin) Main() {
87 | cfg := a.getOpts()
88 | a.init()
89 | go rpc.ListenAndServe(cfg.App.RPCListenAddr, NewSrv(a))
90 | app := newApp(a)
91 | app.Run(iris.Addr(cfg.App.HTTPListenAddr))
92 | }
93 |
--------------------------------------------------------------------------------
/jiacrontab_admin/app.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "net/url"
5 | "sync/atomic"
6 |
7 | "github.com/kataras/iris/v12"
8 | "github.com/kataras/iris/v12/middleware/logger"
9 |
10 | "jiacrontab/models"
11 |
12 | "fmt"
13 |
14 | jwt "github.com/dgrijalva/jwt-go"
15 | "github.com/iris-contrib/middleware/cors"
16 | jwtmiddleware "github.com/iris-contrib/middleware/jwt"
17 | "github.com/kataras/iris/v12/context"
18 | )
19 |
20 | func newApp(adm *Admin) *iris.Application {
21 |
22 | app := iris.New()
23 | app.UseGlobal(newRecover(adm))
24 | app.Logger().SetLevel(adm.getOpts().App.LogLevel)
25 | app.Use(logger.New())
26 | app.HandleDir("/", AssetFile(), iris.DirOptions{
27 | IndexName: "index.html",
28 | Cache: iris.DirCacheOptions{
29 | Enable: true,
30 | CompressIgnore: iris.MatchImagesAssets,
31 | Encodings: []string{"gzip", "deflate", "br", "snappy"},
32 | CompressMinSize: 50,
33 | Verbose: 1,
34 | },
35 | })
36 | // app.StaticEmbeddedGzip("/", "./assets/", GzipAsset, GzipAssetNames)
37 | cfg := adm.getOpts()
38 |
39 | wrapHandler := func(h func(ctx *myctx)) context.Handler {
40 | return func(c iris.Context) {
41 | h(wrapCtx(c, adm))
42 | }
43 | }
44 |
45 | jwtHandler := jwtmiddleware.New(jwtmiddleware.Config{
46 | ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
47 | return []byte(cfg.Jwt.SigningKey), nil
48 | },
49 |
50 | Extractor: func(ctx iris.Context) (string, error) {
51 | token, err := url.QueryUnescape(ctx.GetHeader(cfg.Jwt.Name))
52 | return token, err
53 | },
54 | Expiration: true,
55 |
56 | ErrorHandler: func(c iris.Context, err error) {
57 | ctx := wrapCtx(c, adm)
58 | if ctx.RequestPath(true) != "/user/login" {
59 | ctx.respAuthFailed(fmt.Errorf("Token verification failed(%s)", err))
60 | return
61 | }
62 | ctx.Next()
63 | },
64 |
65 | SigningMethod: jwt.SigningMethodHS256,
66 | })
67 |
68 | crs := cors.New(cors.Options{
69 | Debug: false,
70 | AllowedHeaders: []string{"Content-Type", "Token"},
71 | AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts.
72 | AllowCredentials: true,
73 | })
74 |
75 | app.Use(crs)
76 | app.AllowMethods(iris.MethodOptions)
77 |
78 | v1 := app.Party("/v1")
79 | {
80 | v1.Post("/user/login", wrapHandler(Login))
81 | v1.Post("/app/init", wrapHandler(InitApp))
82 | }
83 |
84 | v2 := app.Party("/v2")
85 | {
86 | v2.Use(jwtHandler.Serve)
87 | v2.Use(wrapHandler(func(ctx *myctx) {
88 | if err := ctx.parseClaimsFromToken(); err != nil {
89 | ctx.respJWTError(err)
90 | return
91 | }
92 | ctx.Next()
93 | }))
94 | v2.Post("/crontab/job/list", wrapHandler(GetJobList))
95 | v2.Post("/crontab/job/get", wrapHandler(GetJob))
96 | v2.Post("/crontab/job/log", wrapHandler(GetRecentLog))
97 | v2.Post("/crontab/job/edit", wrapHandler(EditJob))
98 | v2.Post("/crontab/job/action", wrapHandler(ActionTask))
99 | v2.Post("/crontab/job/exec", wrapHandler(ExecTask))
100 |
101 | v2.Post("/config/get", wrapHandler(GetConfig))
102 | v2.Post("/config/mail/send", wrapHandler(SendTestMail))
103 | v2.Post("/system/info", wrapHandler(SystemInfo))
104 | v2.Post("/log/info", wrapHandler(LogInfo))
105 | v2.Post("/log/clean", wrapHandler(CleanLog))
106 |
107 | v2.Post("/daemon/job/list", wrapHandler(GetDaemonJobList))
108 | v2.Post("/daemon/job/action", wrapHandler(ActionDaemonTask))
109 | v2.Post("/daemon/job/edit", wrapHandler(EditDaemonJob))
110 | v2.Post("/daemon/job/get", wrapHandler(GetDaemonJob))
111 | v2.Post("/daemon/job/log", wrapHandler(GetRecentDaemonLog))
112 |
113 | v2.Post("/group/list", wrapHandler(GetGroupList))
114 | v2.Post("/group/edit", wrapHandler(EditGroup))
115 |
116 | v2.Post("/node/list", wrapHandler(GetNodeList))
117 | v2.Post("/node/delete", wrapHandler(DeleteNode))
118 | v2.Post("/node/group_node", wrapHandler(GroupNode))
119 | v2.Post("/node/clean_log", wrapHandler(CleanNodeLog))
120 |
121 | v2.Post("/user/activity_list", wrapHandler(GetActivityList))
122 | v2.Post("/user/job_history", wrapHandler(GetJobHistory))
123 | v2.Post("/user/audit_job", wrapHandler(AuditJob))
124 | v2.Post("/user/stat", wrapHandler(UserStat))
125 | v2.Post("/user/signup", wrapHandler(Signup))
126 | v2.Post("/user/edit", wrapHandler(EditUser))
127 | v2.Post("/user/delete", wrapHandler(DeleteUser))
128 | v2.Post("/user/group_user", wrapHandler(GroupUser))
129 | v2.Post("/user/list", wrapHandler(GetUserList))
130 | }
131 |
132 | debug := app.Party("/debug")
133 | {
134 | debug.Get("/stat", wrapHandler(stat))
135 | debug.Get("/pprof/", wrapHandler(indexDebug))
136 | debug.Get("/pprof/{key:string}", wrapHandler(pprofHandler))
137 | }
138 |
139 | return app
140 | }
141 |
142 | // InitApp 初始化应用
143 | func InitApp(ctx *myctx) {
144 | var (
145 | err error
146 | user models.User
147 | reqBody InitAppReqParams
148 | )
149 |
150 | if err = ctx.Valid(&reqBody); err != nil {
151 | ctx.respParamError(err)
152 | return
153 | }
154 |
155 | if ret := models.DB().Take(&user, "group_id=?", 1); ret.Error == nil && ret.RowsAffected > 0 {
156 | ctx.respNotAllowed()
157 | return
158 | }
159 |
160 | user.Username = reqBody.Username
161 | user.Passwd = reqBody.Passwd
162 | user.Root = true
163 | user.GroupID = models.SuperGroup.ID
164 | user.Mail = reqBody.Mail
165 |
166 | if err = user.Create(); err != nil {
167 | ctx.respBasicError(err)
168 | return
169 | }
170 | atomic.StoreInt32(&ctx.adm.initAdminUser, 1)
171 | ctx.respSucc("", true)
172 | }
173 |
--------------------------------------------------------------------------------
/jiacrontab_admin/config.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "jiacrontab/pkg/file"
6 | "jiacrontab/pkg/mailer"
7 | "reflect"
8 | "time"
9 |
10 | "github.com/iwannay/log"
11 |
12 | ini "gopkg.in/ini.v1"
13 | )
14 |
15 | const (
16 | appname = "jiacrontab"
17 | )
18 |
19 | type AppOpt struct {
20 | HTTPListenAddr string `opt:"http_listen_addr"`
21 | RPCListenAddr string `opt:"rpc_listen_addr"`
22 | AppName string `opt:"app_name" `
23 | Debug bool `opt:"debug" `
24 | LogLevel string `opt:"log_level"`
25 | SigningKey string `opt:"signing_key"`
26 | MaxClientAliveInterval int `opt:"max_client_alive_interval"`
27 | }
28 |
29 | type JwtOpt struct {
30 | SigningKey string `opt:"signing_key"`
31 | Name string `opt:"name" `
32 | Expires int64 `opt:"expires"`
33 | }
34 |
35 | type MailerOpt struct {
36 | Enabled bool `opt:"enabled"`
37 | QueueLength int `opt:"queue_length"`
38 | SubjectPrefix string `opt:"subject_Prefix"`
39 | Host string `opt:"host"`
40 | From string `opt:"from"`
41 | FromEmail string `opt:"from_email"`
42 | User string `opt:"user"`
43 | Passwd string `opt:"passwd"`
44 | DisableHelo bool `opt:"disable_helo"`
45 | HeloHostname string `opt:"helo_hostname"`
46 | SkipVerify bool `opt:"skip_verify"`
47 | UseCertificate bool `opt:"use_certificate"`
48 | CertFile string `opt:"cert_file"`
49 | KeyFile string `opt:"key_file"`
50 | UsePlainText bool `opt:"use_plain_text"`
51 | }
52 |
53 | type databaseOpt struct {
54 | DriverName string `opt:"driver_name"`
55 | DSN string `opt:"dsn"`
56 | }
57 |
58 | type ldapOpt struct {
59 | Addr string `opt:"addr"`
60 | DisabledAnonymousQuery bool `opt:"disabled_anonymous_query"`
61 | Basedn string `opt:"basedn"`
62 | Timeout int `opt:"timeout"`
63 | BindPasswd string `opt:"bind_passwd"`
64 | BindUserdn string `opt:"bind_userdn"`
65 | UserField string `opt:"user_field"`
66 | }
67 |
68 | type Config struct {
69 | Mailer *MailerOpt `section:"mail"`
70 | Jwt *JwtOpt `section:"jwt"`
71 | App *AppOpt `section:"app"`
72 | Database *databaseOpt `section:"database"`
73 | Ldap *ldapOpt `section:"ldap"`
74 |
75 | CfgPath string
76 | iniFile *ini.File
77 | ServerStartTime time.Time `json:"-"`
78 | }
79 |
80 | func (c *Config) Resolve() {
81 |
82 | c.ServerStartTime = time.Now()
83 | c.iniFile = c.loadConfig(c.CfgPath)
84 |
85 | val := reflect.ValueOf(c).Elem()
86 | typ := val.Type()
87 | for i := 0; i < typ.NumField(); i++ {
88 | field := typ.Field(i)
89 | section := field.Tag.Get("section")
90 | if section == "" {
91 | continue
92 | }
93 | subVal := reflect.ValueOf(val.Field(i).Interface()).Elem()
94 | subtyp := subVal.Type()
95 | for j := 0; j < subtyp.NumField(); j++ {
96 | subField := subtyp.Field(j)
97 | subOpt := subField.Tag.Get("opt")
98 | if subOpt == "" {
99 | continue
100 | }
101 | sec := c.iniFile.Section(section)
102 |
103 | if !sec.HasKey(subOpt) {
104 | continue
105 | }
106 |
107 | key := sec.Key(subOpt)
108 |
109 | switch subField.Type.Kind() {
110 | case reflect.Bool:
111 | v, err := key.Bool()
112 | if err != nil {
113 | log.Error(err)
114 | continue
115 | }
116 | subVal.Field(j).SetBool(v)
117 | case reflect.String:
118 | subVal.Field(j).SetString(key.String())
119 | case reflect.Int64:
120 | v, err := key.Int64()
121 | if err != nil {
122 | log.Error(err)
123 | continue
124 | }
125 | subVal.Field(j).SetInt(v)
126 | }
127 | }
128 | }
129 | }
130 |
131 | func NewConfig() *Config {
132 | return &Config{
133 | App: &AppOpt{
134 | Debug: false,
135 | HTTPListenAddr: ":20000",
136 | RPCListenAddr: ":20003",
137 | AppName: "jiacrontab",
138 | LogLevel: "warn",
139 | SigningKey: "WERRTT1234$@#@@$",
140 | MaxClientAliveInterval: 30,
141 | },
142 | Mailer: &MailerOpt{
143 | Enabled: false,
144 | QueueLength: 1000,
145 | SubjectPrefix: "jiacrontab",
146 | SkipVerify: true,
147 | UseCertificate: false,
148 | },
149 | Jwt: &JwtOpt{
150 | SigningKey: "ADSFdfs2342$@@#",
151 | Name: "token",
152 | Expires: 3600,
153 | },
154 | Ldap: &ldapOpt{},
155 | Database: &databaseOpt{},
156 | }
157 | }
158 |
159 | func (c *Config) loadConfig(path string) *ini.File {
160 | if !file.Exist(path) {
161 | f, err := file.CreateFile(path)
162 | if err != nil {
163 | panic(err)
164 | }
165 | f.Close()
166 | }
167 |
168 | iniFile, err := ini.Load(path)
169 | if err != nil {
170 | panic(err)
171 | }
172 | return iniFile
173 | }
174 |
175 | func GetConfig(ctx *myctx) {
176 | cfg := ctx.adm.getOpts()
177 | if !ctx.isSuper() {
178 | ctx.respNotAllowed()
179 | return
180 | }
181 | ctx.respSucc("", map[string]interface{}{
182 | "mail": map[string]interface{}{
183 | "host": cfg.Mailer.Host,
184 | "user": cfg.Mailer.User,
185 | "use_certificate": cfg.Mailer.UseCertificate,
186 | "skip_verify": cfg.Mailer.SkipVerify,
187 | "cert_file": cfg.Mailer.CertFile,
188 | "key_file": cfg.Mailer.KeyFile,
189 | },
190 | })
191 | }
192 |
193 | func SendTestMail(ctx *myctx) {
194 | var (
195 | err error
196 | reqBody SendTestMailReqParams
197 | cfg = ctx.adm.getOpts()
198 | )
199 |
200 | if err = ctx.Valid(&reqBody); err != nil {
201 | ctx.respParamError(err)
202 | return
203 | }
204 |
205 | if cfg.Mailer.Enabled {
206 | err = mailer.SendMail([]string{reqBody.MailTo}, "jiacrontab欢迎你", "来自jiacrontab的温馨祝福!")
207 | if err != nil {
208 | ctx.respBasicError(err)
209 | return
210 | }
211 | ctx.respSucc("", nil)
212 | return
213 | }
214 |
215 | ctx.respBasicError(errors.New("邮箱服务未开启"))
216 | }
217 |
--------------------------------------------------------------------------------
/jiacrontab_admin/const.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | const (
4 | event_DelNodeDesc = "{username}删除了节点{targetName}"
5 | event_RenameNode = "{username}将{sourceName}重命名为{targetName}"
6 |
7 | event_EditCronJob = "{sourceName}{username}编辑了定时任务{targetName}"
8 | event_DelCronJob = "{sourceName}{username}删除了定时任务{targetName}"
9 | event_StopCronJob = "{sourceName}{username}停止了定时任务{targetName}"
10 | event_StartCronJob = "{sourceName}{username}启动了定时任务{targetName}"
11 | event_ExecCronJob = "{sourceName}{username}执行了定时任务{targetName}"
12 | event_KillCronJob = "{sourceName}{username}kill了定时任务进程{targetName}"
13 |
14 | event_EditDaemonJob = "{sourceName}{username}编辑了常驻任务{targetName}"
15 | event_DelDaemonJob = "{sourceName}{username}删除了常驻任务{targetName}"
16 | event_StartDaemonJob = "{sourceName}{username}启动了常驻任务{targetName}"
17 | event_StopDaemonJob = "{sourceName}{username}停止了常驻任务{targetName}"
18 |
19 | event_EditGroup = "{username}编辑了{targetName}组"
20 | event_GroupNode = "{username}将节点{sourceName}添加到{targetName}组"
21 |
22 | event_SignUpUser = "{username}创建了用户{targetName}"
23 | event_EditUser = "{username}更新了用户信息"
24 | event_DeleteUser = "{username}删除了用户{targetName}"
25 | event_GroupUser = "{username}将用户{sourceUsername}设置为{targetName}组"
26 | event_AuditCrontabJob = "{sourceName}{username}审核了定时任务{targetName}"
27 | event_AuditDaemonJob = "{sourceName}{username}审核了常驻任务{targetName}"
28 |
29 | event_CleanJobHistory = "{username}清除了{targetName}前的任务执行记录"
30 | event_CleanUserEvent = "{username}清除了{targetName}前的用户动态"
31 | event_CleanNodeLog = "{sourceName}{username}清除了{targetName}前的job动态"
32 | )
33 |
--------------------------------------------------------------------------------
/jiacrontab_admin/crontab.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "jiacrontab/models"
6 | "jiacrontab/pkg/proto"
7 | "strings"
8 | )
9 |
10 | func GetJobList(ctx *myctx) {
11 |
12 | var (
13 | jobRet proto.QueryCrontabJobRet
14 | err error
15 | reqBody GetJobListReqParams
16 | rpcReqParams proto.QueryJobArgs
17 | )
18 |
19 | if err = ctx.Valid(&reqBody); err != nil {
20 | ctx.respParamError(err)
21 | return
22 | }
23 |
24 | rpcReqParams.Page = reqBody.Page
25 | rpcReqParams.Pagesize = reqBody.Pagesize
26 | rpcReqParams.UserID = ctx.claims.UserID
27 | rpcReqParams.Root = ctx.claims.Root
28 | rpcReqParams.GroupID = ctx.claims.GroupID
29 | rpcReqParams.SearchTxt = reqBody.SearchTxt
30 |
31 | if err := rpcCall(reqBody.Addr, "CrontabJob.List", rpcReqParams, &jobRet); err != nil {
32 | ctx.respRPCError(err)
33 | return
34 | }
35 |
36 | ctx.respSucc("", map[string]interface{}{
37 | "list": jobRet.List,
38 | "page": jobRet.Page,
39 | "pagesize": jobRet.Pagesize,
40 | "total": jobRet.Total,
41 | })
42 | }
43 |
44 | func GetRecentLog(ctx *myctx) {
45 | var (
46 | err error
47 | searchRet proto.SearchLogResult
48 | reqBody GetLogReqParams
49 | logList []string
50 | )
51 |
52 | if err = ctx.Valid(&reqBody); err != nil {
53 | ctx.respParamError(err)
54 | return
55 | }
56 |
57 | if err = rpcCall(reqBody.Addr, "CrontabJob.Log", proto.SearchLog{
58 | JobID: reqBody.JobID,
59 | Offset: reqBody.Offset,
60 | Pagesize: reqBody.Pagesize,
61 | Date: reqBody.Date,
62 | Root: ctx.claims.Root,
63 | GroupID: ctx.claims.GroupID,
64 | UserID: ctx.claims.UserID,
65 | Pattern: reqBody.Pattern,
66 | IsTail: reqBody.IsTail,
67 | }, &searchRet); err != nil {
68 | ctx.respRPCError(err)
69 | return
70 | }
71 |
72 | logList = strings.Split(string(searchRet.Content), "\n")
73 |
74 | ctx.respSucc("", map[string]interface{}{
75 | "logList": logList,
76 | "curAddr": reqBody.Addr,
77 | "offset": searchRet.Offset,
78 | "filesize": searchRet.FileSize,
79 | "pagesize": reqBody.Pagesize,
80 | })
81 | }
82 |
83 | func EditJob(ctx *myctx) {
84 | var (
85 | err error
86 | reply models.CrontabJob
87 | reqBody EditJobReqParams
88 | job models.CrontabJob
89 | )
90 |
91 | if err = ctx.Valid(&reqBody); err != nil {
92 | ctx.respBasicError(err)
93 | return
94 | }
95 |
96 | if !ctx.verifyNodePermission(reqBody.Addr) {
97 | ctx.respNotAllowed()
98 | return
99 | }
100 |
101 | job = models.CrontabJob{
102 | Name: reqBody.Name,
103 | Command: reqBody.Command,
104 | GroupID: ctx.claims.GroupID,
105 | Code: reqBody.Code,
106 | TimeArgs: models.TimeArgs{
107 | Month: reqBody.Month,
108 | Day: reqBody.Day,
109 | Hour: reqBody.Hour,
110 | Minute: reqBody.Minute,
111 | Weekday: reqBody.Weekday,
112 | Second: reqBody.Second,
113 | },
114 |
115 | UpdatedUserID: ctx.claims.UserID,
116 | UpdatedUsername: ctx.claims.Username,
117 | WorkDir: reqBody.WorkDir,
118 | WorkUser: reqBody.WorkUser,
119 | WorkIp: reqBody.WorkIp,
120 | WorkEnv: reqBody.WorkEnv,
121 | KillChildProcess: reqBody.KillChildProcess,
122 | RetryNum: reqBody.RetryNum,
123 | Timeout: reqBody.Timeout,
124 | TimeoutTrigger: reqBody.TimeoutTrigger,
125 | MailTo: reqBody.MailTo,
126 | APITo: reqBody.APITo,
127 | DingdingTo: reqBody.DingdingTo,
128 | MaxConcurrent: reqBody.MaxConcurrent,
129 | DependJobs: reqBody.DependJobs,
130 | ErrorMailNotify: reqBody.ErrorMailNotify,
131 | ErrorAPINotify: reqBody.ErrorAPINotify,
132 | ErrorDingdingNotify: reqBody.ErrorDingdingNotify,
133 | IsSync: reqBody.IsSync,
134 | CreatedUserID: ctx.claims.UserID,
135 | CreatedUsername: ctx.claims.Username,
136 | }
137 |
138 | job.ID = reqBody.JobID
139 | if ctx.claims.Root || ctx.claims.GroupID == models.SuperGroup.ID {
140 | job.Status = models.StatusJobOk
141 | } else {
142 | job.Status = models.StatusJobUnaudited
143 | }
144 |
145 | if err = rpcCall(reqBody.Addr, "CrontabJob.Edit", proto.EditCrontabJobArgs{
146 | Job: job,
147 | GroupID: ctx.claims.GroupID,
148 | Root: ctx.claims.Root,
149 | UserID: ctx.claims.UserID,
150 | }, &reply); err != nil {
151 | ctx.respRPCError(err)
152 | return
153 | }
154 | ctx.pubEvent(reply.Name, event_EditCronJob, models.EventSourceName(reqBody.Addr), reqBody)
155 | ctx.respSucc("", reply)
156 | }
157 |
158 | func ActionTask(ctx *myctx) {
159 | var (
160 | err error
161 | ok bool
162 | method string
163 | reqBody ActionTaskReqParams
164 | jobReply []models.CrontabJob
165 | methods = map[string]string{
166 | "start": "CrontabJob.Start",
167 | "stop": "CrontabJob.Stop",
168 | "delete": "CrontabJob.Delete",
169 | "batch-exec": "CrontabJob.Execs",
170 | "kill": "CrontabJob.Kill",
171 | }
172 |
173 | eDesc = map[string]string{
174 | "start": event_StartCronJob,
175 | "stop": event_StopCronJob,
176 | "batch-exec": event_ExecCronJob,
177 | "delete": event_DelCronJob,
178 | "kill": event_KillCronJob,
179 | }
180 | )
181 |
182 | if err = ctx.Valid(&reqBody); err != nil {
183 | ctx.respBasicError(err)
184 | return
185 | }
186 |
187 | if method, ok = methods[reqBody.Action]; !ok {
188 | ctx.respBasicError(errors.New("action无法识别"))
189 | return
190 | }
191 |
192 | if err = rpcCall(reqBody.Addr, method, proto.ActionJobsArgs{
193 | UserID: ctx.claims.UserID,
194 | GroupID: ctx.claims.GroupID,
195 | Root: ctx.claims.Root,
196 | JobIDs: reqBody.JobIDs,
197 | }, &jobReply); err != nil {
198 | ctx.respRPCError(err)
199 | return
200 | }
201 | if len(jobReply) > 0 {
202 | var targetNames []string
203 | for _, v := range jobReply {
204 | targetNames = append(targetNames, v.Name)
205 | }
206 | ctx.pubEvent(strings.Join(targetNames, ","), eDesc[reqBody.Action], models.EventSourceName(reqBody.Addr), reqBody)
207 | }
208 | ctx.respSucc("", jobReply)
209 | }
210 |
211 | func ExecTask(ctx *myctx) {
212 | var (
213 | err error
214 | logList []string
215 | execJobReply proto.ExecCrontabJobReply
216 | reqBody JobReqParams
217 | )
218 |
219 | if err = ctx.Valid(&reqBody); err != nil {
220 | ctx.respParamError(err)
221 | return
222 | }
223 |
224 | if err = rpcCall(reqBody.Addr, "CrontabJob.Exec", proto.GetJobArgs{
225 | UserID: ctx.claims.UserID,
226 | Root: ctx.claims.Root,
227 | JobID: reqBody.JobID,
228 | GroupID: ctx.claims.GroupID,
229 | }, &execJobReply); err != nil {
230 | ctx.respRPCError(err)
231 | return
232 | }
233 | logList = strings.Split(string(execJobReply.Content), "\n")
234 | ctx.pubEvent(execJobReply.Job.Name, event_ExecCronJob, models.EventSourceName(reqBody.Addr), reqBody)
235 | ctx.respSucc("", logList)
236 | }
237 |
238 | func GetJob(ctx *myctx) {
239 | var (
240 | reqBody GetJobReqParams
241 | crontabJob models.CrontabJob
242 | err error
243 | )
244 |
245 | if err = ctx.Valid(&reqBody); err != nil {
246 | ctx.respParamError(err)
247 | return
248 | }
249 |
250 | if !ctx.verifyNodePermission(reqBody.Addr) {
251 | ctx.respNotAllowed()
252 | return
253 | }
254 |
255 | if err = rpcCall(reqBody.Addr, "CrontabJob.Get", proto.GetJobArgs{
256 | UserID: ctx.claims.UserID,
257 | Root: ctx.claims.Root,
258 | GroupID: ctx.claims.GroupID,
259 | JobID: reqBody.JobID,
260 | }, &crontabJob); err != nil {
261 | ctx.respRPCError(err)
262 | return
263 | }
264 |
265 | ctx.respSucc("", crontabJob)
266 | }
267 |
--------------------------------------------------------------------------------
/jiacrontab_admin/ctx.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "jiacrontab/models"
9 | "jiacrontab/pkg/proto"
10 | "net/http"
11 | "strings"
12 | "sync/atomic"
13 |
14 | "jiacrontab/pkg/version"
15 |
16 | "github.com/iwannay/log"
17 |
18 | jwt "github.com/dgrijalva/jwt-go"
19 | "github.com/kataras/iris/v12"
20 | )
21 |
22 | type myctx struct {
23 | iris.Context
24 | adm *Admin
25 | claims CustomerClaims
26 | }
27 |
28 | func wrapCtx(ctx iris.Context, adm *Admin) *myctx {
29 | key := "__ctx__"
30 | if v := ctx.Values().Get(key); v != nil {
31 | return v.(*myctx)
32 | }
33 |
34 | c := &myctx{
35 | Context: ctx,
36 | adm: adm,
37 | }
38 | if atomic.LoadInt32(&adm.initAdminUser) == 1 {
39 | ctx.SetCookieKV("ready", "true", func(ctx iris.Context, c *http.Cookie, op uint8) {
40 | if op == 1 {
41 | c.HttpOnly = false
42 | }
43 | })
44 | } else {
45 | ctx.SetCookieKV("ready", "false", func(ctx iris.Context, c *http.Cookie, op uint8) {
46 | if op == 1 {
47 | c.HttpOnly = false
48 | }
49 | })
50 | }
51 | ctx.Values().Set(key, c)
52 | return c
53 | }
54 |
55 | func (ctx *myctx) respNotAllowed() {
56 | ctx.respError(proto.Code_NotAllowed, proto.Msg_NotAllowed)
57 | }
58 |
59 | func (ctx *myctx) respAuthFailed(err error) {
60 | ctx.respError(proto.Code_FailedAuth, err)
61 | }
62 |
63 | func (ctx *myctx) respDBError(err error) {
64 | ctx.respError(proto.Code_DBError, err)
65 | }
66 |
67 | func (ctx *myctx) respJWTError(err error) {
68 | ctx.respError(proto.Code_JWTError, err)
69 | }
70 |
71 | func (ctx *myctx) respBasicError(err error) {
72 | ctx.respError(proto.Code_Error, err)
73 | }
74 |
75 | func (ctx *myctx) respParamError(err error) {
76 | ctx.respError(proto.Code_ParamsError, err)
77 | }
78 |
79 | func (ctx *myctx) respRPCError(err error) {
80 | ctx.respError(proto.Code_RPCError, err)
81 | }
82 |
83 | func (ctx *myctx) respError(code int, err interface{}, v ...interface{}) {
84 |
85 | var (
86 | sign string
87 | bts []byte
88 | msgStr string
89 | data interface{}
90 | cfg = ctx.adm.getOpts()
91 | )
92 |
93 | if err == nil {
94 | msgStr = "error"
95 | }
96 | msgStr = fmt.Sprintf("%s", err)
97 | if len(v) >= 1 {
98 | data = v[0]
99 | }
100 |
101 | if strings.Contains(msgStr, "UNIQUE constraint failed") {
102 | msgStr = strings.Replace(msgStr, "UNIQUE constraint failed", "数据已存在,请检查索引", -1)
103 | }
104 |
105 | bts, err = json.Marshal(data)
106 | if err != nil {
107 | log.Error("errorResp:", err)
108 | }
109 |
110 | sign = fmt.Sprintf("%x", md5.Sum(append(bts, []byte(cfg.App.SigningKey)...)))
111 |
112 | ctx.JSON(proto.Resp{
113 | Code: code,
114 | Msg: msgStr,
115 | Data: json.RawMessage(bts),
116 | Sign: sign,
117 | Version: version.String(cfg.App.AppName),
118 | })
119 | }
120 |
121 | func (ctx *myctx) respSucc(msg string, v interface{}) {
122 |
123 | cfg := ctx.adm.getOpts()
124 | if msg == "" {
125 | msg = "success"
126 | }
127 |
128 | bts, err := json.Marshal(v)
129 | if err != nil {
130 | log.Error("errorResp:", err)
131 | }
132 |
133 | sign := fmt.Sprintf("%x", md5.Sum(append(bts, []byte(cfg.App.SigningKey)...)))
134 |
135 | ctx.JSON(proto.Resp{
136 | Code: proto.SuccessRespCode,
137 | Msg: msg,
138 | Data: json.RawMessage(bts),
139 | Sign: sign,
140 | Version: version.String(cfg.App.AppName),
141 | })
142 | }
143 |
144 | func (ctx *myctx) isSuper() bool {
145 | return ctx.claims.GroupID == models.SuperGroup.ID
146 | }
147 | func (ctx *myctx) isRoot() bool {
148 | return ctx.claims.Root
149 | }
150 |
151 | func (ctx *myctx) parseClaimsFromToken() error {
152 | var ok bool
153 |
154 | if (ctx.claims != CustomerClaims{}) {
155 | return nil
156 | }
157 |
158 | token, ok := ctx.Values().Get("jwt").(*jwt.Token)
159 | if !ok {
160 | return errors.New("claims is nil")
161 | }
162 | bts, err := json.Marshal(token.Claims)
163 | if err != nil {
164 | return err
165 | }
166 | err = json.Unmarshal(bts, &ctx.claims)
167 | if err != nil {
168 | return fmt.Errorf("unmarshal claims error - (%s)", err)
169 | }
170 | var user models.User
171 | if err := models.DB().Take(&user, "id=?", ctx.claims.UserID).Error; err != nil {
172 | return fmt.Errorf("validate user from db error - (%s)", err)
173 | }
174 | if ctx.claims.Mail != user.Mail || ctx.claims.GroupID != user.GroupID || ctx.claims.Root != user.Root || ctx.claims.Version != user.Version {
175 | return fmt.Errorf("token validate error")
176 | }
177 |
178 | if ctx.claims.GroupID == models.SuperGroup.ID {
179 | ctx.claims.Root = true
180 | }
181 |
182 | return nil
183 | }
184 |
185 | func (ctx *myctx) getGroupNodes() ([]models.Node, error) {
186 | var nodes []models.Node
187 | err := models.DB().Find(&nodes, "group_id=?", ctx.claims.GroupID).Error
188 | return nodes, err
189 | }
190 |
191 | func (ctx *myctx) verifyNodePermission(addr string) bool {
192 | var node models.Node
193 | return node.VerifyUserGroup(ctx.claims.UserID, ctx.claims.GroupID, addr)
194 | }
195 |
196 | func (ctx *myctx) getGroupAddr() ([]string, error) {
197 | var addrs []string
198 | nodes, err := ctx.getGroupNodes()
199 | if err != nil {
200 | return nil, err
201 | }
202 |
203 | for _, v := range nodes {
204 | addrs = append(addrs, v.Addr)
205 | }
206 | return addrs, nil
207 |
208 | }
209 |
210 | func (ctx *myctx) Valid(i Parameter) error {
211 | if err := ctx.ReadJSON(i); err != nil {
212 | return err
213 | }
214 |
215 | if err := i.Verify(ctx); err != nil {
216 | return err
217 | }
218 |
219 | if err := validStructRule(i); err != nil {
220 | return err
221 | }
222 | return nil
223 | }
224 |
225 | func (ctx *myctx) pubEvent(targetName, desc string, source interface{}, v interface{}) {
226 | var content string
227 |
228 | if v != nil {
229 | bts, err := json.Marshal(v)
230 | if err != nil {
231 | return
232 | }
233 | content = string(bts)
234 | }
235 |
236 | e := models.Event{
237 | GroupID: ctx.claims.GroupID,
238 | UserID: ctx.claims.UserID,
239 | Username: ctx.claims.Username,
240 | EventDesc: desc,
241 | TargetName: targetName,
242 | Content: content,
243 | }
244 |
245 | switch v := source.(type) {
246 | case models.EventSourceName:
247 | e.SourceName = string(v)
248 | case models.EventSourceUsername:
249 | e.SourceUsername = string(v)
250 | }
251 |
252 | e.Pub()
253 | }
254 |
--------------------------------------------------------------------------------
/jiacrontab_admin/daemon.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "jiacrontab/models"
6 | "jiacrontab/pkg/proto"
7 | "jiacrontab/pkg/rpc"
8 | "strings"
9 | )
10 |
11 | func GetDaemonJobList(ctx *myctx) {
12 | var (
13 | reqBody GetJobListReqParams
14 | jobRet proto.QueryDaemonJobRet
15 | err error
16 | )
17 |
18 | if err = ctx.Valid(&reqBody); err != nil {
19 | ctx.respParamError(err)
20 | return
21 | }
22 |
23 | if err = rpcCall(reqBody.Addr, "DaemonJob.List", &proto.QueryJobArgs{
24 | Page: reqBody.Page,
25 | Pagesize: reqBody.Pagesize,
26 | SearchTxt: reqBody.SearchTxt,
27 | Root: ctx.claims.Root,
28 | GroupID: ctx.claims.GroupID,
29 | UserID: ctx.claims.UserID,
30 | }, &jobRet); err != nil {
31 | ctx.respRPCError(err)
32 | return
33 | }
34 |
35 | ctx.respSucc("", map[string]interface{}{
36 | "list": jobRet.List,
37 | "page": jobRet.Page,
38 | "pagesize": jobRet.Pagesize,
39 | "total": jobRet.Total,
40 | })
41 | }
42 |
43 | func ActionDaemonTask(ctx *myctx) {
44 | var (
45 | err error
46 | reply []models.DaemonJob
47 | ok bool
48 | reqBody ActionTaskReqParams
49 |
50 | methods = map[string]string{
51 | "start": "DaemonJob.Start",
52 | "delete": "DaemonJob.Delete",
53 | "stop": "DaemonJob.Stop",
54 | }
55 |
56 | eDesc = map[string]string{
57 | "start": event_StartDaemonJob,
58 | "delete": event_DelDaemonJob,
59 | "stop": event_StopDaemonJob,
60 | }
61 | method string
62 | )
63 |
64 | if err = ctx.Valid(&reqBody); err != nil {
65 | ctx.respBasicError(err)
66 | }
67 |
68 | if method, ok = methods[reqBody.Action]; !ok {
69 | ctx.respBasicError(errors.New("参数错误"))
70 | return
71 | }
72 |
73 | if err = rpcCall(reqBody.Addr, method, proto.ActionJobsArgs{
74 | UserID: ctx.claims.UserID,
75 | JobIDs: reqBody.JobIDs,
76 | GroupID: ctx.claims.GroupID,
77 | Root: ctx.claims.Root,
78 | }, &reply); err != nil {
79 | ctx.respRPCError(err)
80 | return
81 | }
82 |
83 | if len(reply) > 0 {
84 | var targetNames []string
85 | for _, v := range reply {
86 | targetNames = append(targetNames, v.Name)
87 | }
88 | ctx.pubEvent(strings.Join(targetNames, ","), eDesc[reqBody.Action], models.EventSourceName(reqBody.Addr), reqBody)
89 | }
90 |
91 | ctx.respSucc("", nil)
92 | }
93 |
94 | // EditDaemonJob 修改常驻任务,jobID为0时新增
95 | func EditDaemonJob(ctx *myctx) {
96 | var (
97 | err error
98 | reply models.DaemonJob
99 | reqBody EditDaemonJobReqParams
100 | daemonJob models.DaemonJob
101 | )
102 |
103 | if err = ctx.Valid(&reqBody); err != nil {
104 | ctx.respParamError(err)
105 | }
106 |
107 | if !ctx.verifyNodePermission(reqBody.Addr) {
108 | ctx.respNotAllowed()
109 | return
110 | }
111 |
112 | daemonJob = models.DaemonJob{
113 | Name: reqBody.Name,
114 | GroupID: ctx.claims.GroupID,
115 | ErrorMailNotify: reqBody.ErrorMailNotify,
116 | ErrorAPINotify: reqBody.ErrorAPINotify,
117 | ErrorDingdingNotify: reqBody.ErrorDingdingNotify,
118 | MailTo: reqBody.MailTo,
119 | APITo: reqBody.APITo,
120 | DingdingTo: reqBody.DingdingTo,
121 | UpdatedUserID: ctx.claims.UserID,
122 | UpdatedUsername: ctx.claims.Username,
123 | Command: reqBody.Command,
124 | WorkDir: reqBody.WorkDir,
125 | WorkEnv: reqBody.WorkEnv,
126 | WorkUser: reqBody.WorkUser,
127 | WorkIp: reqBody.WorkIp,
128 | Code: reqBody.Code,
129 | RetryNum: reqBody.RetryNum,
130 | FailRestart: reqBody.FailRestart,
131 | Status: models.StatusJobUnaudited,
132 | CreatedUserID: ctx.claims.UserID,
133 | CreatedUsername: ctx.claims.Username,
134 | }
135 | daemonJob.ID = reqBody.JobID
136 | if ctx.claims.Root || ctx.claims.GroupID == models.SuperGroup.ID {
137 | daemonJob.Status = models.StatusJobOk
138 | } else {
139 | daemonJob.Status = models.StatusJobUnaudited
140 | }
141 |
142 | if err = rpcCall(reqBody.Addr, "DaemonJob.Edit", proto.EditDaemonJobArgs{
143 | GroupID: ctx.claims.GroupID,
144 | UserID: ctx.claims.UserID,
145 | Job: daemonJob,
146 | Root: ctx.claims.Root,
147 | }, &reply); err != nil {
148 | ctx.respRPCError(err)
149 | return
150 | }
151 |
152 | ctx.pubEvent(reply.Name, event_EditDaemonJob, models.EventSourceName(reqBody.Addr), reqBody)
153 | ctx.respSucc("", reply)
154 | }
155 |
156 | func GetDaemonJob(ctx *myctx) {
157 | var (
158 | reqBody GetJobReqParams
159 | daemonJob models.DaemonJob
160 | err error
161 | )
162 |
163 | if err = ctx.Valid(&reqBody); err != nil {
164 | ctx.respParamError(err)
165 | return
166 | }
167 |
168 | if !ctx.verifyNodePermission(reqBody.Addr) {
169 | ctx.respNotAllowed()
170 | return
171 | }
172 |
173 | if err = rpcCall(reqBody.Addr, "DaemonJob.Get", proto.GetJobArgs{
174 | UserID: ctx.claims.UserID,
175 | GroupID: ctx.claims.GroupID,
176 | Root: ctx.claims.Root,
177 | JobID: reqBody.JobID,
178 | }, &daemonJob); err != nil {
179 | ctx.respRPCError(err)
180 | return
181 | }
182 |
183 | ctx.respSucc("", daemonJob)
184 | }
185 |
186 | func GetRecentDaemonLog(ctx *myctx) {
187 | var (
188 | err error
189 | searchRet proto.SearchLogResult
190 | reqBody GetLogReqParams
191 | logList []string
192 | )
193 |
194 | if err = ctx.Valid(&reqBody); err != nil {
195 | ctx.respParamError(err)
196 | return
197 | }
198 |
199 | if err := rpc.Call(reqBody.Addr, "DaemonJob.Log", proto.SearchLog{
200 | JobID: reqBody.JobID,
201 | GroupID: ctx.claims.GroupID,
202 | Root: ctx.claims.Root,
203 | UserID: ctx.claims.UserID,
204 | Offset: reqBody.Offset,
205 | Pagesize: reqBody.Pagesize,
206 | Date: reqBody.Date,
207 | Pattern: reqBody.Pattern,
208 | IsTail: reqBody.IsTail,
209 | }, &searchRet); err != nil {
210 | ctx.respRPCError(err)
211 | return
212 | }
213 |
214 | logList = strings.Split(string(searchRet.Content), "\n")
215 |
216 | ctx.respSucc("", map[string]interface{}{
217 | "logList": logList,
218 | "curAddr": reqBody.Addr,
219 | "offset": searchRet.Offset,
220 | "filesize": searchRet.FileSize,
221 | "pagesize": reqBody.Pagesize,
222 | })
223 | }
224 |
--------------------------------------------------------------------------------
/jiacrontab_admin/debug.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "jiacrontab/pkg/base"
5 | "net/http/pprof"
6 | )
7 |
8 | func stat(ctx *myctx) {
9 | data := base.Stat.Collect()
10 | ctx.JSON(data)
11 | }
12 |
13 | func pprofHandler(ctx *myctx) {
14 | if h := pprof.Handler(ctx.Params().Get("key")); h != nil {
15 | h.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
16 | }
17 | }
18 |
19 | func indexDebug(ctx *myctx) {
20 | pprof.Index(ctx.ResponseWriter(), ctx.Request())
21 | }
22 |
--------------------------------------------------------------------------------
/jiacrontab_admin/group.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "jiacrontab/models"
5 | "jiacrontab/pkg/proto"
6 | )
7 |
8 | // GetGroupList 获得所有的分组列表
9 | func GetGroupList(ctx *myctx) {
10 | var (
11 | err error
12 | groupList []models.Group
13 | count int64
14 | model = models.DB().Model(&models.Group{})
15 | reqBody GetGroupListReqParams
16 | )
17 |
18 | if err = ctx.Valid(&reqBody); err != nil {
19 | ctx.respParamError(err)
20 | return
21 | }
22 |
23 | if !ctx.isSuper() {
24 | ctx.respNotAllowed()
25 | return
26 | }
27 |
28 | model.Where("name like ?", "%"+reqBody.SearchTxt+"%").Count(&count)
29 |
30 | err = model.Where("name like ?", "%"+reqBody.SearchTxt+"%").Offset((reqBody.Page - 1) * reqBody.Pagesize).Order("updated_at desc").Limit(reqBody.Pagesize).Find(&groupList).Error
31 | if err != nil {
32 | ctx.respError(proto.Code_Error, err.Error(), nil)
33 | return
34 | }
35 |
36 | ctx.respSucc("", map[string]interface{}{
37 | "list": groupList,
38 | "total": count,
39 | "page": reqBody.Page,
40 | "pagesize": reqBody.Pagesize,
41 | })
42 | }
43 |
44 | // EditGroup 编辑分组
45 | func EditGroup(ctx *myctx) {
46 | var (
47 | reqBody EditGroupReqParams
48 | err error
49 | group models.Group
50 | )
51 |
52 | if err = ctx.Valid(&reqBody); err != nil {
53 | ctx.respParamError(err)
54 | return
55 | }
56 |
57 | group.ID = reqBody.GroupID
58 | group.Name = reqBody.GroupName
59 |
60 | if err = group.Save(); err != nil {
61 | ctx.respBasicError(err)
62 | return
63 | }
64 | ctx.pubEvent(group.Name, event_EditGroup, "", reqBody)
65 | ctx.respSucc("", nil)
66 | }
67 |
--------------------------------------------------------------------------------
/jiacrontab_admin/ldap.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "fmt"
5 | "jiacrontab/models"
6 | "time"
7 |
8 | "errors"
9 |
10 | ld "github.com/go-ldap/ldap/v3"
11 | )
12 |
13 | type Ldap struct {
14 | BindUserDn string
15 | BindPwd string
16 | BaseOn string
17 | UserField string
18 | Addr string
19 | Timeout time.Duration
20 | fields []map[string]string
21 | queryFields []string
22 | lastSynced time.Time
23 | DisabledAnonymousQuery bool
24 | }
25 |
26 | func (l *Ldap) connect() (*ld.Conn, error) {
27 | var err error
28 | conn, err := ld.DialURL(l.Addr)
29 | if err != nil {
30 | return nil, err
31 | }
32 | conn.SetTimeout(l.Timeout)
33 | return conn, nil
34 | }
35 |
36 | func (l *Ldap) Login(username string, password string) (*models.User, error) {
37 | err := l.loadLdapFields()
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | conn, err := l.connect()
43 | if err != nil {
44 | return nil, err
45 | }
46 | defer conn.Close()
47 |
48 | if l.DisabledAnonymousQuery {
49 | err := conn.Bind(l.BindUserDn, l.BindPwd)
50 | if err != nil {
51 | return nil, err
52 | }
53 | }
54 | query := ld.NewSearchRequest(
55 | l.BaseOn,
56 | ld.ScopeWholeSubtree,
57 | ld.DerefAlways,
58 | 0, 0, false,
59 | fmt.Sprintf("(%v=%v)", l.UserField, username),
60 | l.queryFields, nil,
61 | )
62 | ret, err := conn.Search(query)
63 |
64 | if err != nil {
65 | return nil, fmt.Errorf("在Ldap搜索用户失败 - %v", err)
66 | }
67 | if len(ret.Entries) == 0 {
68 | return nil, fmt.Errorf("在Ldap中未查询到对应的用户信息 - %v", err)
69 | }
70 |
71 | err = conn.Bind(ret.Entries[0].DN, password)
72 | if err != nil {
73 | return nil, errors.New("帐号或密码不正确")
74 | }
75 | return l.convert(ret.Entries[0])
76 | }
77 |
78 | func (l *Ldap) loadLdapFields() error {
79 | var setting models.SysSetting
80 | var list []map[string]string
81 |
82 | if time.Since(l.lastSynced).Hours() < 1 {
83 | return nil
84 | }
85 |
86 | err := models.DB().Model(&models.SysSetting{}).Where("class=?", 1).Find(&setting).Error
87 | if err != nil {
88 | return err
89 | }
90 | // err = json.Unmarshal(setting.Content, &list)
91 | // if err != nil {
92 | // return err
93 | // }
94 | for _, v := range list {
95 | if v["ldap_field_name"] != "" {
96 | l.fields = append(l.fields, v)
97 | }
98 | }
99 |
100 | l.queryFields = []string{"dn"}
101 | for _, v := range l.fields {
102 | l.queryFields = append(l.queryFields, v["ldap_field_name"])
103 | }
104 |
105 | return nil
106 | }
107 |
108 | func (l *Ldap) convert(ldapUserInfo *ld.Entry) (*models.User, error) {
109 | var userinfo models.User
110 | for _, v := range l.fields {
111 | switch v["local_field_name"] {
112 | case "username":
113 | userinfo.Username = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
114 | case "gender":
115 | userinfo.Gender = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
116 | case "avatar":
117 | userinfo.Avatar = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
118 | case "email":
119 | userinfo.Mail = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
120 | }
121 | }
122 | err := models.DB().Where("username=?", userinfo.Username).FirstOrCreate(&userinfo).Error
123 | return &userinfo, err
124 | }
125 |
--------------------------------------------------------------------------------
/jiacrontab_admin/node.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "fmt"
5 | "jiacrontab/models"
6 | "jiacrontab/pkg/proto"
7 | "time"
8 | )
9 |
10 | // GetNodeList 获得任务节点列表
11 | // 支持获得所属分组节点,指定分组节点(超级管理员)
12 | func GetNodeList(ctx *myctx) {
13 | var (
14 | err error
15 | nodeList []models.Node
16 | reqBody GetNodeListReqParams
17 | count int64
18 | )
19 |
20 | if err = ctx.Valid(&reqBody); err != nil {
21 | ctx.respParamError(err)
22 | return
23 | }
24 |
25 | if reqBody.QueryGroupID != ctx.claims.GroupID && !ctx.isSuper() {
26 | ctx.respNotAllowed()
27 | return
28 | }
29 |
30 | cfg := ctx.adm.getOpts()
31 | maxClientAliveInterval := -1 * cfg.App.MaxClientAliveInterval
32 |
33 | currentTime := time.Now().Add(time.Second * time.Duration(maxClientAliveInterval)).Format("2006-01-02 15:04:05")
34 |
35 | //失联列表更新为已断开状态
36 | models.DB().Unscoped().Model(&models.Node{}).Where("updated_at", currentTime).Updates(map[string]interface{}{
37 | "disabled": true,
38 | })
39 |
40 | model := models.DB()
41 | if reqBody.SearchTxt != "" {
42 | txt := "%" + reqBody.SearchTxt + "%"
43 | model = model.Where("(name like ? or addr like ?)", txt, txt)
44 | }
45 |
46 | switch reqBody.QueryStatus {
47 | case 1:
48 | err = model.Preload("Group").Where("group_id=? and disabled=?",
49 | reqBody.QueryGroupID, false).Offset((reqBody.Page - 1) * reqBody.Pagesize).
50 | Order("id desc").Limit(reqBody.Pagesize).Find(&nodeList).Error
51 |
52 | model.Model(&models.Node{}).Where("group_id=? and disabled=?",
53 | reqBody.QueryGroupID, false).Count(&count)
54 | case 2:
55 | err = model.Preload("Group").Where("group_id=? and disabled=?",
56 | reqBody.QueryGroupID, true).Offset((reqBody.Page - 1) * reqBody.Pagesize).Order("id desc").Limit(reqBody.Pagesize).Find(&nodeList).Error
57 |
58 | model.Model(&models.Node{}).Where("group_id=? and disabled=?", reqBody.QueryGroupID, true).Count(&count)
59 | default:
60 | err = model.Preload("Group").Where("group_id=?",
61 | reqBody.QueryGroupID).Offset((reqBody.Page - 1) * reqBody.Pagesize).Order("id desc").Limit(reqBody.Pagesize).Find(&nodeList).Error
62 |
63 | model.Model(&models.Node{}).Where("group_id=?",
64 | reqBody.QueryGroupID).Count(&count)
65 | }
66 |
67 | if err != nil {
68 | ctx.respBasicError(err)
69 | return
70 | }
71 |
72 | ctx.respSucc("", map[string]interface{}{
73 | "list": nodeList,
74 | "total": count,
75 | "page": reqBody.Page,
76 | "pagesize": reqBody.Pagesize,
77 | })
78 | }
79 |
80 | // DeleteNode 删除分组内节点
81 | // 仅超级管理员有权限
82 | func DeleteNode(ctx *myctx) {
83 | var (
84 | err error
85 | reqBody DeleteNodeReqParams
86 | node models.Node
87 | )
88 |
89 | if err = ctx.Valid(&reqBody); err != nil {
90 | ctx.respParamError(err)
91 | }
92 |
93 | // 普通用户不允许删除节点
94 | if !(ctx.isSuper() || (ctx.isRoot() && ctx.claims.GroupID == reqBody.GroupID)) {
95 | ctx.respNotAllowed()
96 | return
97 | }
98 |
99 | if err = node.Delete(reqBody.GroupID, reqBody.Addr); err != nil {
100 | ctx.respDBError(err)
101 | return
102 | }
103 |
104 | ctx.pubEvent(node.Addr, event_DelNodeDesc, models.EventSourceName(reqBody.Addr), reqBody)
105 | ctx.respSucc("", nil)
106 | }
107 |
108 | // GroupNode 超级管理员为node分组
109 | // 分组不存在时自动创建分组
110 | // copy超级管理员分组中的节点到新的分组
111 | func GroupNode(ctx *myctx) {
112 | var (
113 | err error
114 | reqBody GroupNodeReqParams
115 | node models.Node
116 | )
117 |
118 | if !ctx.isSuper() {
119 | ctx.respNotAllowed()
120 | return
121 | }
122 |
123 | if err = ctx.Valid(&reqBody); err != nil {
124 | ctx.respParamError(err)
125 | return
126 | }
127 |
128 | if err = node.GroupNode(reqBody.Addr, reqBody.TargetGroupID,
129 | reqBody.TargetNodeName, reqBody.TargetGroupName); err != nil {
130 | ctx.respBasicError(err)
131 | return
132 | }
133 |
134 | ctx.pubEvent(node.Group.Name, event_GroupNode, models.EventSourceName(reqBody.Addr), reqBody)
135 | ctx.respSucc("", nil)
136 | }
137 |
138 | // DeleteNode 删除分组内节点
139 | // 仅超级管理员有权限
140 | func CleanNodeLog(ctx *myctx) {
141 | var (
142 | err error
143 | reqBody CleanNodeLogReqParams
144 | cleanRet proto.CleanNodeLogRet
145 | )
146 |
147 | if err = ctx.Valid(&reqBody); err != nil {
148 | ctx.respParamError(err)
149 | return
150 | }
151 |
152 | // 普通用户不允许删除节点
153 | if !ctx.isSuper() {
154 | ctx.respNotAllowed()
155 | return
156 | }
157 |
158 | if err = rpcCall(reqBody.Addr, "Srv.CleanLogFiles", proto.CleanNodeLog{
159 | Unit: reqBody.Unit,
160 | Offset: reqBody.Offset,
161 | }, &cleanRet); err != nil {
162 | ctx.respRPCError(err)
163 | return
164 | }
165 |
166 | ctx.pubEvent(fmt.Sprintf("%d %s", reqBody.Offset, reqBody.Unit), event_CleanNodeLog, models.EventSourceName(reqBody.Addr), reqBody)
167 | ctx.respSucc("", cleanRet)
168 | }
169 |
--------------------------------------------------------------------------------
/jiacrontab_admin/params.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "jiacrontab/models"
7 | "jiacrontab/pkg/proto"
8 | "jiacrontab/pkg/util"
9 | "strings"
10 | )
11 |
12 | var (
13 | paramsError = errors.New("参数错误")
14 | )
15 |
16 | type Parameter interface {
17 | Verify(*myctx) error
18 | }
19 |
20 | type JobReqParams struct {
21 | JobID uint `json:"jobID" rule:"required,请填写jobID"`
22 | Addr string `json:"addr" rule:"required,请填写addr"`
23 | }
24 |
25 | func (p *JobReqParams) Verify(*myctx) error {
26 | if p.JobID == 0 || p.Addr == "" {
27 | return paramsError
28 | }
29 | return nil
30 | }
31 |
32 | type JobsReqParams struct {
33 | JobIDs []uint `json:"jobIDs" `
34 | Addr string `json:"addr"`
35 | }
36 |
37 | func (p *JobsReqParams) Verify(ctx *myctx) error {
38 |
39 | if len(p.JobIDs) == 0 || p.Addr == "" {
40 | return paramsError
41 | }
42 |
43 | return nil
44 | }
45 |
46 | type EditJobReqParams struct {
47 | JobID uint `json:"jobID"`
48 | Addr string `json:"addr" rule:"required,请填写addr"`
49 | IsSync bool `json:"isSync"`
50 | Name string `json:"name" rule:"required,请填写name"`
51 | Command []string `json:"command" rule:"required,请填写name"`
52 | Code string `json:"code"`
53 | Timeout int `json:"timeout"`
54 | MaxConcurrent uint `json:"maxConcurrent"`
55 | ErrorMailNotify bool `json:"errorMailNotify"`
56 | ErrorAPINotify bool `json:"errorAPINotify"`
57 | ErrorDingdingNotify bool `json:"errorDingdingNotify"`
58 | MailTo []string `json:"mailTo"`
59 | APITo []string `json:"APITo"`
60 | DingdingTo []string `json:"DingdingTo"`
61 | RetryNum int `json:"retryNum"`
62 | WorkDir string `json:"workDir"`
63 | WorkUser string `json:"workUser"`
64 | WorkEnv []string `json:"workEnv"`
65 | WorkIp []string `json:"workIp"`
66 | KillChildProcess bool `json:"killChildProcess"`
67 | DependJobs models.DependJobs `json:"dependJobs"`
68 | Month string `json:"month"`
69 | Weekday string `json:"weekday"`
70 | Day string `json:"day"`
71 | Hour string `json:"hour"`
72 | Minute string `json:"minute"`
73 | Second string `json:"second"`
74 | TimeoutTrigger []string `json:"timeoutTrigger"`
75 | }
76 |
77 | func (p *EditJobReqParams) Verify(ctx *myctx) error {
78 | ts := map[string]bool{
79 | proto.TimeoutTrigger_CallApi: true,
80 | proto.TimeoutTrigger_SendEmail: true,
81 | proto.TimeoutTrigger_Kill: true,
82 | proto.TimeoutTrigger_DingdingWebhook: true,
83 | }
84 |
85 | for _, v := range p.TimeoutTrigger {
86 | if !ts[v] {
87 | return fmt.Errorf("%s:%v", v, paramsError)
88 | }
89 | }
90 |
91 | p.Command = util.FilterEmptyEle(p.Command)
92 | p.MailTo = util.FilterEmptyEle(p.MailTo)
93 | p.APITo = util.FilterEmptyEle(p.APITo)
94 | p.DingdingTo = util.FilterEmptyEle(p.DingdingTo)
95 | p.WorkEnv = util.FilterEmptyEle(p.WorkEnv)
96 | p.WorkIp = util.FilterEmptyEle(p.WorkIp)
97 |
98 | if p.Month == "" {
99 | p.Month = "*"
100 | }
101 |
102 | if p.Weekday == "" {
103 | p.Weekday = "*"
104 | }
105 |
106 | if p.Day == "" {
107 | p.Day = "*"
108 | }
109 |
110 | if p.Hour == "" {
111 | p.Hour = "*"
112 | }
113 |
114 | if p.Minute == "" {
115 | p.Minute = "*"
116 | }
117 |
118 | if p.Second == "" {
119 | p.Second = "*"
120 | }
121 |
122 | return nil
123 | }
124 |
125 | type GetLogReqParams struct {
126 | Addr string `json:"addr"`
127 | JobID uint `json:"jobID"`
128 | Date string `json:"date"`
129 | Pattern string `json:"pattern"`
130 | IsTail bool `json:"isTail"`
131 | Offset int64 `json:"offset"`
132 | Pagesize int `json:"pagesize"`
133 | }
134 |
135 | func (p *GetLogReqParams) Verify(ctx *myctx) error {
136 |
137 | if p.Pagesize <= 0 {
138 | p.Pagesize = 50
139 | }
140 |
141 | return nil
142 | }
143 |
144 | type DeleteNodeReqParams struct {
145 | Addr string `json:"addr" rule:"required,请填写addr"`
146 | GroupID uint `json:"groupID"`
147 | }
148 |
149 | func (p *DeleteNodeReqParams) Verify(ctx *myctx) error {
150 | return nil
151 | }
152 |
153 | type CleanNodeLogReqParams struct {
154 | Unit string `json:"unit" rule:"required,请填写时间单位"`
155 | Offset int `json:"offset"`
156 | Addr string `json:"addr" rule:"required,请填写addr"`
157 | }
158 |
159 | func (p *CleanNodeLogReqParams) Verify(ctx *myctx) error {
160 | if p.Unit != "day" && p.Unit != "month" {
161 | return errors.New("不支持的时间单位")
162 | }
163 | return nil
164 | }
165 |
166 | type SendTestMailReqParams struct {
167 | MailTo string `json:"mailTo" rule:"required,请填写mailTo"`
168 | }
169 |
170 | func (p *SendTestMailReqParams) Verify(ctx *myctx) error {
171 | return nil
172 | }
173 |
174 | type SystemInfoReqParams struct {
175 | Addr string `json:"addr" rule:"required,请填写addr"`
176 | }
177 |
178 | func (p *SystemInfoReqParams) Verify(ctx *myctx) error {
179 | return nil
180 | }
181 |
182 | type GetJobListReqParams struct {
183 | Addr string `json:"addr" rule:"required,请填写addr"`
184 | SearchTxt string `json:"searchTxt"`
185 | PageReqParams
186 | }
187 |
188 | func (p *GetJobListReqParams) Verify(ctx *myctx) error {
189 |
190 | if p.Page <= 1 {
191 | p.Page = 1
192 | }
193 |
194 | if p.Pagesize <= 0 {
195 | p.Pagesize = 50
196 | }
197 | return nil
198 | }
199 |
200 | type GetGroupListReqParams struct {
201 | SearchTxt string `json:"searchTxt"`
202 | PageReqParams
203 | }
204 |
205 | func (p *GetGroupListReqParams) Verify(ctx *myctx) error {
206 |
207 | if p.Page <= 1 {
208 | p.Page = 1
209 | }
210 |
211 | if p.Pagesize <= 0 {
212 | p.Pagesize = 50
213 | }
214 | return nil
215 | }
216 |
217 | type ActionTaskReqParams struct {
218 | Action string `json:"action" rule:"required,请填写action"`
219 | Addr string `json:"addr" rule:"required,请填写addr"`
220 | JobIDs []uint `json:"jobIDs" rule:"required,请填写jobIDs"`
221 | }
222 |
223 | func (p *ActionTaskReqParams) Verify(ctx *myctx) error {
224 | if len(p.JobIDs) == 0 {
225 | return paramsError
226 | }
227 | return nil
228 | }
229 |
230 | type EditDaemonJobReqParams struct {
231 | Addr string `json:"addr" rule:"required,请填写addr"`
232 | JobID uint `json:"jobID"`
233 | Name string `json:"name" rule:"required,请填写name"`
234 | MailTo []string `json:"mailTo"`
235 | APITo []string `json:"APITo"`
236 | DingdingTo []string `json:"DingdingTo"`
237 | Command []string `json:"command" rule:"required,请填写command"`
238 | Code string `json:"code"`
239 | WorkUser string `json:"workUser"`
240 | WorkIp []string `json:"workIp"`
241 | WorkEnv []string `json:"workEnv"`
242 | WorkDir string `json:"workDir"`
243 | FailRestart bool `json:"failRestart"`
244 | RetryNum int `json:"retryNum"`
245 | ErrorMailNotify bool `json:"errorMailNotify"`
246 | ErrorAPINotify bool `json:"errorAPINotify"`
247 | ErrorDingdingNotify bool `json:"errorDingdingNotify"`
248 | }
249 |
250 | func (p *EditDaemonJobReqParams) Verify(ctx *myctx) error {
251 | p.MailTo = util.FilterEmptyEle(p.MailTo)
252 | p.APITo = util.FilterEmptyEle(p.APITo)
253 | p.Command = util.FilterEmptyEle(p.Command)
254 | p.WorkEnv = util.FilterEmptyEle(p.WorkEnv)
255 | p.WorkIp = util.FilterEmptyEle(p.WorkIp)
256 | return nil
257 | }
258 |
259 | type GetJobReqParams struct {
260 | JobID uint `json:"jobID" rule:"required,请填写jobID"`
261 | Addr string `json:"addr" rule:"required,请填写addr"`
262 | }
263 |
264 | func (p *GetJobReqParams) Verify(ctx *myctx) error {
265 | return nil
266 | }
267 |
268 | type UserReqParams struct {
269 | Username string `json:"username" rule:"required,请输入用户名"`
270 | Passwd string `json:"passwd,omitempty" rule:"required,请输入密码"`
271 | GroupID uint `json:"groupID"`
272 | GroupName string `json:"groupName"`
273 | Avatar string `json:"avatar"`
274 | Root bool `json:"root"`
275 | Mail string `json:"mail"`
276 | }
277 |
278 | func (p *UserReqParams) Verify(ctx *myctx) error {
279 | return nil
280 | }
281 |
282 | type InitAppReqParams struct {
283 | Username string `json:"username" rule:"required,请输入用户名"`
284 | Passwd string `json:"passwd" rule:"required,请输入密码"`
285 | Avatar string `json:"avatar"`
286 | Mail string `json:"mail"`
287 | }
288 |
289 | func (p *InitAppReqParams) Verify(ctx *myctx) error {
290 | return nil
291 | }
292 |
293 | type EditUserReqParams struct {
294 | UserID uint `json:"userID" rule:"required,缺少userID"`
295 | Username string `json:"username"`
296 | Passwd string `json:"passwd"`
297 | OldPwd string `json:"oldpwd"`
298 | Avatar string `json:"avatar"`
299 | Mail string `json:"mail"`
300 | }
301 |
302 | func (p *EditUserReqParams) Verify(ctx *myctx) error {
303 | return nil
304 | }
305 |
306 | type DeleteUserReqParams struct {
307 | UserID uint `json:"userID" rule:"required,缺少userID"`
308 | }
309 |
310 | func (p *DeleteUserReqParams) Verify(ctx *myctx) error {
311 | return nil
312 | }
313 |
314 | type LoginReqParams struct {
315 | Username string `json:"username" rule:"required,请输入用户名"`
316 | Passwd string `json:"passwd" rule:"required,请输入密码"`
317 | Remember bool `json:"remember"`
318 | IsLdap bool `json:"is_ldap"`
319 | }
320 |
321 | func (p *LoginReqParams) Verify(ctx *myctx) error {
322 | return nil
323 | }
324 |
325 | type PageReqParams struct {
326 | Page int `json:"page"`
327 | Pagesize int `json:"pagesize"`
328 | }
329 |
330 | type GetNodeListReqParams struct {
331 | PageReqParams
332 | SearchTxt string `json:"searchTxt"`
333 | QueryGroupID uint `json:"queryGroupID"`
334 | QueryStatus uint `json:"queryStatus"`
335 | }
336 |
337 | func (p *GetNodeListReqParams) Verify(ctx *myctx) error {
338 |
339 | if p.Page == 0 {
340 | p.Page = 1
341 | }
342 | if p.Pagesize <= 0 {
343 | p.Pagesize = 50
344 | }
345 | return nil
346 | }
347 |
348 | type EditGroupReqParams struct {
349 | GroupID uint `json:"groupID" rule:"required,请填写groupID"`
350 | GroupName string `json:"groupName" rule:"required,请填写groupName"`
351 | }
352 |
353 | func (p *EditGroupReqParams) Verify(ctx *myctx) error {
354 | return nil
355 | }
356 |
357 | type SetGroupReqParams struct {
358 | TargetGroupID uint `json:"targetGroupID"`
359 | TargetGroupName string `json:"targetGroupName"`
360 | UserID uint `json:"userID" rule:"required,请填写用户ID"`
361 | Root bool `json:"root"`
362 | }
363 |
364 | func (p *SetGroupReqParams) Verify(ctx *myctx) error {
365 | return nil
366 | }
367 |
368 | type ReadMoreReqParams struct {
369 | LastID int `json:"lastID"`
370 | Pagesize int `json:"pagesize"`
371 | Keywords string `json:"keywords"`
372 | Orderby string `json:"orderby"`
373 | }
374 |
375 | func (p *ReadMoreReqParams) Verify(ctx *myctx) error {
376 | if p.Pagesize == 0 {
377 | p.Pagesize = 50
378 | }
379 |
380 | if p.Orderby == "" {
381 | p.Orderby = "desc"
382 | }
383 |
384 | p.Keywords = strings.TrimSpace(p.Keywords)
385 | return nil
386 | }
387 |
388 | type GroupNodeReqParams struct {
389 | Addr string `json:"addr" rule:"required,请填写addr"`
390 | TargetNodeName string `json:"targetNodeName"`
391 | TargetGroupName string `json:"targetGroupName"`
392 | TargetGroupID uint `json:"targetGroupID"`
393 | }
394 |
395 | func (p *GroupNodeReqParams) Verify(ctx *myctx) error {
396 | return nil
397 | }
398 |
399 | type AuditJobReqParams struct {
400 | JobsReqParams
401 | JobType string `json:"jobType"`
402 | }
403 |
404 | func (p *AuditJobReqParams) Verify(ctx *myctx) error {
405 |
406 | if p.Addr == "" {
407 | return paramsError
408 | }
409 |
410 | jobTypeMap := map[string]bool{
411 | "crontab": true,
412 | "daemon": true,
413 | }
414 |
415 | if err := p.JobsReqParams.Verify(nil); err != nil {
416 | return err
417 | }
418 |
419 | if !jobTypeMap[p.JobType] {
420 | return paramsError
421 | }
422 |
423 | return nil
424 | }
425 |
426 | type GetUsersParams struct {
427 | PageReqParams
428 | SearchTxt string `json:"searchTxt"`
429 | IsAll bool `json:"isAll"`
430 | QueryGroupID uint `json:"queryGroupID"`
431 | }
432 |
433 | func (p *GetUsersParams) Verify(ctx *myctx) error {
434 |
435 | if p.Page <= 1 {
436 | p.Page = 1
437 | }
438 |
439 | if p.Pagesize <= 0 {
440 | p.Pagesize = 50
441 | }
442 | return nil
443 | }
444 |
445 | type CleanLogParams struct {
446 | IsEvent bool `json:"isEvent"`
447 | Unit string `json:"unit" rule:"required,请填写时间单位"`
448 | Offset int `json:"offset"`
449 | }
450 |
451 | func (c *CleanLogParams) Verify(ctx *myctx) error {
452 | if c.Unit != "day" && c.Unit != "month" {
453 | return errors.New("不支持的时间单位")
454 | }
455 | return nil
456 | }
457 |
--------------------------------------------------------------------------------
/jiacrontab_admin/recover.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "fmt"
5 | "jiacrontab/pkg/base"
6 | "net/http"
7 | "runtime"
8 | "strconv"
9 |
10 | "github.com/kataras/iris/v12"
11 | )
12 |
13 | func getRequestLogs(ctx *myctx) string {
14 | var status, ip, method, path string
15 | status = strconv.Itoa(ctx.GetStatusCode())
16 | path = ctx.Path()
17 | method = ctx.Method()
18 | ip = ctx.RemoteAddr()
19 | // the date should be logged by iris' Logger, so we skip them
20 | return fmt.Sprintf("%v %s %s %s", status, path, method, ip)
21 | }
22 |
23 | func newRecover(adm *Admin) iris.Handler {
24 | return func(c iris.Context) {
25 | ctx := wrapCtx(c, adm)
26 | base.Stat.AddConcurrentCount()
27 | defer func() {
28 | if err := recover(); err != nil {
29 |
30 | base.Stat.AddErrorCount(ctx.RequestPath(true), fmt.Errorf("%v", err), 1)
31 |
32 | if ctx.IsStopped() {
33 | return
34 | }
35 |
36 | var stacktrace string
37 | for i := 1; ; i++ {
38 | _, f, l, got := runtime.Caller(i)
39 | if !got {
40 | break
41 |
42 | }
43 |
44 | stacktrace += fmt.Sprintf("%s:%d\n", f, l)
45 | }
46 |
47 | // when stack finishes
48 | logMessage := fmt.Sprintf("Recovered from a route's Handler('%s')\n", ctx.HandlerName())
49 | logMessage += fmt.Sprintf("At Request: %s\n", getRequestLogs(ctx))
50 | logMessage += fmt.Sprintf("Trace: %s\n", err)
51 | logMessage += fmt.Sprintf("\n%s", stacktrace)
52 | ctx.Application().Logger().Warn(logMessage)
53 |
54 | ctx.StatusCode(500)
55 | ctx.respError(http.StatusInternalServerError, fmt.Sprint(err), nil)
56 | ctx.StopExecution()
57 | }
58 | }()
59 | base.Stat.AddRequestCount(ctx.RequestPath(true), ctx.GetStatusCode(), 1)
60 | c.Next()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/jiacrontab_admin/runtime.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "jiacrontab/pkg/proto"
5 | "jiacrontab/pkg/version"
6 | )
7 |
8 | func SystemInfo(ctx *myctx) {
9 | var (
10 | err error
11 | info map[string]interface{}
12 | reqBody SystemInfoReqParams
13 | )
14 |
15 | if err = ctx.Valid(&reqBody); err != nil {
16 | ctx.respBasicError(err)
17 | return
18 | }
19 |
20 | if err = rpcCall(reqBody.Addr, "Srv.SystemInfo", proto.EmptyArgs{}, &info); err != nil {
21 | ctx.respRPCError(err)
22 | return
23 | }
24 | info["version"] = version.String("jiacrontab")
25 | ctx.respSucc("", info)
26 | }
27 |
--------------------------------------------------------------------------------
/jiacrontab_admin/srv.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "jiacrontab/models"
8 | "jiacrontab/pkg/mailer"
9 | "jiacrontab/pkg/proto"
10 | "net/http"
11 | "strings"
12 | "time"
13 |
14 | "github.com/iwannay/log"
15 | )
16 |
17 | type Srv struct {
18 | adm *Admin
19 | }
20 |
21 | func NewSrv(adm *Admin) *Srv {
22 | return &Srv{
23 | adm: adm,
24 | }
25 | }
26 |
27 | func (s *Srv) Register(args map[uint]models.Node, reply *bool) error {
28 | *reply = true
29 |
30 | for _, node := range args {
31 | ret := models.DB().Unscoped().Model(&models.Node{}).Where("addr=? and group_id=?", node.Addr, node.GroupID).Updates(map[string]interface{}{
32 | "daemon_task_num": node.DaemonTaskNum,
33 | "crontab_task_num": node.CrontabTaskNum,
34 | "crontab_job_audit_num": node.CrontabJobAuditNum,
35 | "daemon_job_audit_num": node.DaemonJobAuditNum,
36 | "crontab_job_fail_num": node.CrontabJobFailNum,
37 | })
38 | if ret.Error != nil {
39 | return ret.Error
40 | }
41 | if node.GroupID == models.SuperGroup.ID {
42 | ret = models.DB().Unscoped().Model(&models.Node{}).Where("addr=?", node.Addr).Updates(map[string]interface{}{
43 | "name": node.Name,
44 | "deleted_at": nil,
45 | "disabled": false,
46 | })
47 | if ret.Error != nil {
48 | return ret.Error
49 | }
50 | }
51 |
52 | if ret.RowsAffected == 0 && node.GroupID == models.SuperGroup.ID {
53 | ret = models.DB().Create(&node)
54 | }
55 |
56 | if ret.Error != nil {
57 | return ret.Error
58 | }
59 | }
60 |
61 | return nil
62 | }
63 |
64 | func (s *Srv) ExecDepend(args proto.DepJobs, reply *bool) error {
65 | log.Infof("Callee Srv.ExecDepend jobID:%d", args[0].JobID)
66 | *reply = true
67 | for _, v := range args {
68 | if err := rpcCall(v.Dest, "CrontabJob.ExecDepend", v, &reply); err != nil {
69 | *reply = false
70 | return err
71 | }
72 | }
73 |
74 | return nil
75 | }
76 |
77 | func (s *Srv) SetDependDone(args proto.DepJob, reply *bool) error {
78 | log.Infof("Callee Srv.SetDependDone jobID:%d", args.JobID)
79 | *reply = true
80 | if err := rpcCall(args.Dest, "CrontabJob.SetDependDone", args, &reply); err != nil {
81 | *reply = false
82 | return err
83 | }
84 |
85 | return nil
86 | }
87 |
88 | func (s *Srv) SendMail(args proto.SendMail, reply *bool) error {
89 | var (
90 | err error
91 | cfg = s.adm.getOpts()
92 | )
93 | if cfg.Mailer.Enabled {
94 | err = mailer.SendMail(args.MailTo, args.Subject, args.Content)
95 | }
96 | *reply = true
97 | return err
98 | }
99 |
100 | func (s *Srv) PushJobLog(args models.JobHistory, reply *bool) error {
101 | models.PushJobHistory(&args)
102 | *reply = true
103 | return nil
104 | }
105 |
106 | func (s *Srv) ApiPost(args proto.ApiPost, reply *bool) error {
107 | var (
108 | err error
109 | errs []error
110 | )
111 |
112 | for _, url := range args.Urls {
113 |
114 | client := http.Client{
115 | Timeout: time.Minute,
116 | }
117 |
118 | response, err := client.Post(url, "application/json", strings.NewReader(args.Data))
119 |
120 | if err != nil {
121 | errs = append(errs, err)
122 | log.Errorf("post url %s fail: %s", url, err)
123 | continue
124 | }
125 | defer response.Body.Close()
126 | io.Copy(ioutil.Discard, response.Body)
127 | }
128 |
129 | for _, v := range errs {
130 | if err != nil {
131 | err = fmt.Errorf("%s\n%s", err, v)
132 | } else {
133 | err = v
134 | }
135 | }
136 |
137 | *reply = true
138 | return err
139 | }
140 |
141 | func (s *Srv) Ping(args *proto.EmptyArgs, reply *proto.EmptyReply) error {
142 | return nil
143 | }
144 |
--------------------------------------------------------------------------------
/jiacrontab_admin/system.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "fmt"
5 | "jiacrontab/models"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | func LogInfo(ctx *myctx) {
12 | if !ctx.isSuper() {
13 | ctx.respNotAllowed()
14 | return
15 | }
16 | var jobTotal int64
17 | err := models.DB().Model(&models.JobHistory{}).Count(&jobTotal).Error
18 | if err != nil {
19 | ctx.respDBError(err)
20 | }
21 | var eventTotal int64
22 | err = models.DB().Model(&models.Event{}).Count(&eventTotal).Error
23 | if err != nil {
24 | ctx.respDBError(err)
25 | }
26 | ctx.respSucc("", map[string]interface{}{
27 | "event_total": eventTotal,
28 | "job_total": jobTotal,
29 | })
30 | }
31 |
32 | func CleanLog(ctx *myctx) {
33 | var (
34 | err error
35 | reqBody CleanLogParams
36 | isSuper = ctx.isSuper()
37 | )
38 | if err = ctx.Valid(&reqBody); err != nil {
39 | ctx.respParamError(err)
40 | return
41 | }
42 | if !isSuper {
43 | ctx.respNotAllowed()
44 | return
45 | }
46 | offset := time.Now()
47 | if reqBody.Unit == "day" {
48 | offset = offset.AddDate(0, 0, -reqBody.Offset)
49 | }
50 | if reqBody.Unit == "month" {
51 | offset = offset.AddDate(0, -reqBody.Offset, 0)
52 | }
53 | var tx *gorm.DB
54 | if reqBody.IsEvent {
55 | tx = models.DB().Where("created_at", offset).Unscoped().Delete(&models.Event{})
56 |
57 | } else {
58 | tx = models.DB().Where("created_at", offset).Unscoped().Delete(&models.JobHistory{})
59 | }
60 | err = tx.Error
61 | if err != nil {
62 | ctx.respDBError(err)
63 | return
64 |
65 | }
66 | if reqBody.IsEvent {
67 | ctx.pubEvent(fmt.Sprintf("%d %s", reqBody.Offset, reqBody.Unit), event_CleanUserEvent, "", reqBody)
68 | } else {
69 | ctx.pubEvent(fmt.Sprintf("%d %s", reqBody.Offset, reqBody.Unit), event_CleanJobHistory, "", reqBody)
70 | }
71 |
72 | ctx.respSucc("清理成功", map[string]interface{}{
73 | "total": tx.RowsAffected,
74 | })
75 | return
76 | }
77 |
--------------------------------------------------------------------------------
/jiacrontab_admin/util.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "errors"
5 | "jiacrontab/models"
6 | "jiacrontab/pkg/rpc"
7 | "reflect"
8 | "strings"
9 |
10 | "github.com/iwannay/log"
11 | )
12 |
13 | func rpcCall(addr string, serviceMethod string, args interface{}, reply interface{}) error {
14 | err := rpc.Call(addr, serviceMethod, args, reply)
15 | if err != nil {
16 | log.Errorf("rpcCall(%s->%s):%v", addr, serviceMethod, err)
17 | }
18 | if err == rpc.ErrRpc || err == rpc.ErrShutdown {
19 | models.DB().Unscoped().Model(&models.Node{}).Where("addr=?", addr).Update("disabled", true)
20 | }
21 | return err
22 | }
23 |
24 | func validStructRule(i interface{}) error {
25 | rt := reflect.TypeOf(i)
26 | rv := reflect.ValueOf(i)
27 |
28 | if rt.Kind() == reflect.Ptr {
29 | rt = rt.Elem()
30 | }
31 |
32 | if rv.Kind() == reflect.Ptr {
33 | rv = rv.Elem()
34 | }
35 |
36 | for i := 0; i < rt.NumField(); i++ {
37 | sf := rt.Field(i)
38 | r := sf.Tag.Get("rule")
39 | br := sf.Tag.Get("bind")
40 |
41 | if br == "required" && rv.Field(i).Kind() == reflect.Ptr {
42 | if rv.Field(i).IsNil() {
43 | return errors.New(sf.Name + " is required")
44 | }
45 | }
46 |
47 | if r == "" {
48 | continue
49 | }
50 | if rs := strings.Split(r, ","); len(rs) == 2 {
51 | rvf := rv.Field(i)
52 | if rs[0] == "required" {
53 | switch rvf.Kind() {
54 | case reflect.String:
55 | if rvf.Interface() == "" {
56 | return errors.New(rs[1])
57 | }
58 | case reflect.Array, reflect.Map:
59 | if rvf.Len() == 0 {
60 | return errors.New(rs[1])
61 | }
62 |
63 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
64 | if rvf.Interface() == 0 {
65 | return errors.New(rs[1])
66 | }
67 | default:
68 | }
69 |
70 | }
71 | continue
72 | }
73 | }
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/jiacrontabd/cmd.go:
--------------------------------------------------------------------------------
1 | package jiacrontabd
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "context"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "jiacrontab/pkg/kproc"
11 | "jiacrontab/pkg/proto"
12 | "jiacrontab/pkg/util"
13 | "os"
14 | "path/filepath"
15 | "runtime/debug"
16 | "time"
17 |
18 | "github.com/iwannay/log"
19 | )
20 |
21 | type cmdUint struct {
22 | ctx context.Context
23 | id uint
24 | args [][]string
25 | logDir string
26 | content []byte
27 | logFile *os.File
28 | label string
29 | user string
30 | logPath string
31 | verboseLog bool
32 | exportLog bool
33 | ignoreFileLog bool
34 | env []string
35 | ip []string
36 | killChildProcess bool
37 | dir string
38 | startTime time.Time
39 | costTime time.Duration
40 | jd *Jiacrontabd
41 | }
42 |
43 | func (cu *cmdUint) release() {
44 | if cu.logFile != nil {
45 | cu.logFile.Close()
46 | }
47 | cu.costTime = time.Now().Sub(cu.startTime)
48 | }
49 |
50 | func (cu *cmdUint) launch() error {
51 | //todo: 需要添加 ip 校验
52 | defer func() {
53 | if err := recover(); err != nil {
54 | log.Errorf("wrapExecScript error:%v\n%s", err, debug.Stack())
55 | }
56 | cu.release()
57 | }()
58 | cfg := cu.jd.getOpts()
59 | cu.startTime = time.Now()
60 |
61 | var err error
62 |
63 | if err = cu.setLogFile(); err != nil {
64 | return err
65 | }
66 |
67 | if len(cu.args) > 1 {
68 | err = cu.pipeExec()
69 | } else {
70 | err = cu.exec()
71 | }
72 |
73 | if err != nil {
74 | var errMsg string
75 | var prefix string
76 | if cu.verboseLog {
77 | prefix = fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
78 | errMsg = prefix + err.Error() + "\n"
79 | } else {
80 | prefix = fmt.Sprintf("[%s %s] ", cfg.BoardcastAddr, cu.label)
81 | errMsg = prefix + err.Error() + "\n"
82 | }
83 |
84 | cu.writeLog([]byte(errMsg))
85 | if cu.exportLog {
86 | cu.content = append(cu.content, []byte(errMsg)...)
87 | }
88 |
89 | return err
90 | }
91 |
92 | return nil
93 | }
94 |
95 | func (cu *cmdUint) setLogFile() error {
96 | var err error
97 |
98 | if cu.ignoreFileLog {
99 | return nil
100 | }
101 | if cu.logPath == "" {
102 | cu.logPath = filepath.Join(cu.logDir, time.Now().Format("2006/01/02"), fmt.Sprintf("%d.log", cu.id))
103 | }
104 |
105 | cu.logFile, err = util.TryOpen(cu.logPath, os.O_APPEND|os.O_CREATE|os.O_RDWR)
106 | if err != nil {
107 | return err
108 | }
109 | return nil
110 | }
111 |
112 | func (cu *cmdUint) writeLog(b []byte) {
113 | if cu.ignoreFileLog {
114 | return
115 | }
116 | var err error
117 | logPath := filepath.Join(cu.logDir, time.Now().Format("2006/01/02"), fmt.Sprintf("%d.log", cu.id))
118 | if logPath != cu.logPath {
119 | cu.logFile.Close()
120 | cu.logFile, err = util.TryOpen(logPath, os.O_APPEND|os.O_CREATE|os.O_RDWR)
121 | if err != nil {
122 | log.Errorf("writeLog failed - %v", err)
123 | return
124 | }
125 | cu.logPath = logPath
126 | }
127 |
128 | cu.logFile.Write(b)
129 | }
130 |
131 | func (cu *cmdUint) exec() error {
132 | log.Debug("cmd exec args:", cu.args)
133 | if len(cu.args) == 0 {
134 | return errors.New("invalid args")
135 | }
136 | cu.args[0] = util.FilterEmptyEle(cu.args[0])
137 | cmdName := cu.args[0][0]
138 | args := cu.args[0][1:]
139 | cmd := kproc.CommandContext(cu.ctx, cmdName, args...)
140 | cfg := cu.jd.getOpts()
141 |
142 | cmd.SetDir(cu.dir)
143 | cmd.SetEnv(cu.env)
144 | cmd.SetUser(cu.user)
145 | cmd.SetExitKillChildProcess(cu.killChildProcess)
146 |
147 | stdout, err := cmd.StdoutPipe()
148 | if err != nil {
149 | return err
150 | }
151 |
152 | defer stdout.Close()
153 |
154 | stderr, err := cmd.StderrPipe()
155 |
156 | if err != nil {
157 | return err
158 | }
159 |
160 | defer stderr.Close()
161 |
162 | if err := cmd.Start(); err != nil {
163 | return err
164 | }
165 |
166 | reader := bufio.NewReader(stdout)
167 | readerErr := bufio.NewReader(stderr)
168 | // 如果已经存在日志则直接写入
169 | cu.writeLog(cu.content)
170 | go func() {
171 | var (
172 | line []byte
173 | )
174 |
175 | for {
176 |
177 | line, _ = reader.ReadBytes('\n')
178 | if len(line) == 0 {
179 | break
180 | }
181 |
182 | if cfg.VerboseJobLog {
183 | prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
184 | line = append([]byte(prefix), line...)
185 | }
186 |
187 | if cu.exportLog {
188 | cu.content = append(cu.content, line...)
189 | }
190 | cu.writeLog(line)
191 | }
192 |
193 | for {
194 | line, _ = readerErr.ReadBytes('\n')
195 | if len(line) == 0 {
196 | break
197 | }
198 | // 默认给err信息加上日期标志
199 | if cfg.VerboseJobLog {
200 | prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
201 | line = append([]byte(prefix), line...)
202 | }
203 | if cu.exportLog {
204 | cu.content = append(cu.content, line...)
205 | }
206 | cu.writeLog(line)
207 | }
208 | }()
209 |
210 | if err = cmd.Wait(); err != nil {
211 | return err
212 | }
213 |
214 | return nil
215 | }
216 |
217 | func (cu *cmdUint) pipeExec() error {
218 | var (
219 | outBufer bytes.Buffer
220 | errBufer bytes.Buffer
221 | cmdEntryList []*pipeCmd
222 | err, exitError error
223 | line []byte
224 | cfg = cu.jd.getOpts()
225 | )
226 |
227 | for _, v := range cu.args {
228 | v = util.FilterEmptyEle(v)
229 | cmdName := v[0]
230 | args := v[1:]
231 |
232 | cmd := kproc.CommandContext(cu.ctx, cmdName, args...)
233 |
234 | cmd.SetDir(cu.dir)
235 | cmd.SetEnv(cu.env)
236 | cmd.SetUser(cu.user)
237 | cmd.SetExitKillChildProcess(cu.killChildProcess)
238 |
239 | cmdEntryList = append(cmdEntryList, &pipeCmd{cmd})
240 | }
241 |
242 | exitError = execute(&outBufer, &errBufer,
243 | cmdEntryList...,
244 | )
245 |
246 | // 如果已经存在日志则直接写入
247 | cu.writeLog(cu.content)
248 |
249 | for {
250 |
251 | line, err = outBufer.ReadBytes('\n')
252 | if err != nil || err == io.EOF {
253 | break
254 | }
255 | if cfg.VerboseJobLog {
256 | prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
257 | line = append([]byte(prefix), line...)
258 | }
259 |
260 | cu.content = append(cu.content, line...)
261 | cu.writeLog(line)
262 | }
263 |
264 | for {
265 | line, err = errBufer.ReadBytes('\n')
266 | if err != nil || err == io.EOF {
267 | break
268 | }
269 |
270 | if cfg.VerboseJobLog {
271 | prefix := fmt.Sprintf("[%s %s %s] ", time.Now().Format(proto.DefaultTimeLayout), cfg.BoardcastAddr, cu.label)
272 | line = append([]byte(prefix), line...)
273 | }
274 |
275 | if cu.exportLog {
276 | cu.content = append(cu.content, line...)
277 | }
278 | cu.writeLog(line)
279 | }
280 | return exitError
281 | }
282 |
283 | func call(stack []*pipeCmd, pipes []*io.PipeWriter) (err error) {
284 | if stack[0].Process == nil {
285 | if err = stack[0].Start(); err != nil {
286 | return err
287 | }
288 | }
289 |
290 | if len(stack) > 1 {
291 | if err = stack[1].Start(); err != nil {
292 | return err
293 | }
294 |
295 | defer func() {
296 | pipes[0].Close()
297 | if err == nil {
298 | err = call(stack[1:], pipes[1:])
299 | }
300 | if err != nil {
301 | // fixed zombie process
302 | stack[1].Wait()
303 | }
304 | }()
305 | }
306 | return stack[0].Wait()
307 | }
308 |
309 | type pipeCmd struct {
310 | *kproc.KCmd
311 | }
312 |
313 | func execute(outputBuffer *bytes.Buffer, errorBuffer *bytes.Buffer, stack ...*pipeCmd) (err error) {
314 | pipeStack := make([]*io.PipeWriter, len(stack)-1)
315 | i := 0
316 | for ; i < len(stack)-1; i++ {
317 | stdinPipe, stdoutPipe := io.Pipe()
318 | stack[i].Stdout = stdoutPipe
319 | stack[i].Stderr = errorBuffer
320 | stack[i+1].Stdin = stdinPipe
321 | pipeStack[i] = stdoutPipe
322 | }
323 |
324 | stack[i].Stdout = outputBuffer
325 | stack[i].Stderr = errorBuffer
326 |
327 | if err = call(stack, pipeStack); err != nil {
328 | errorBuffer.WriteString(err.Error())
329 | }
330 | return err
331 | }
332 |
--------------------------------------------------------------------------------
/jiacrontabd/config.go:
--------------------------------------------------------------------------------
1 | package jiacrontabd
2 |
3 | import (
4 | "jiacrontab/pkg/file"
5 | "jiacrontab/pkg/util"
6 | "net"
7 | "reflect"
8 |
9 | "github.com/iwannay/log"
10 |
11 | ini "gopkg.in/ini.v1"
12 | )
13 |
14 | const (
15 | appname = "jiacrontabd"
16 | )
17 |
18 | type Config struct {
19 | LogLevel string `opt:"log_level"`
20 | VerboseJobLog bool `opt:"verbose_job_log"`
21 | ListenAddr string `opt:"listen_addr"`
22 | AdminAddr string `opt:"admin_addr"`
23 | LogPath string `opt:"log_path"`
24 | AutoCleanTaskLog bool `opt:"auto_clean_task_log"`
25 | NodeName string `opt:"node_name"`
26 | BoardcastAddr string `opt:"boardcast_addr"`
27 | ClientAliveInterval int `opt:"client_alive_interval"`
28 | CfgPath string
29 | Debug bool `opt:"debug"`
30 | iniFile *ini.File
31 | DriverName string `opt:"driver_name"`
32 | DSN string `opt:"dsn"`
33 | }
34 |
35 | func (c *Config) Resolve() error {
36 | c.iniFile = c.loadConfig(c.CfgPath)
37 |
38 | val := reflect.ValueOf(c).Elem()
39 | typ := val.Type()
40 |
41 | for i := 0; i < typ.NumField(); i++ {
42 | field := typ.Field(i)
43 | opt := field.Tag.Get("opt")
44 | if opt == "" {
45 | continue
46 | }
47 | sec := c.iniFile.Section("jiacrontabd")
48 |
49 | if !sec.HasKey(opt) {
50 | continue
51 | }
52 |
53 | key := sec.Key(opt)
54 | switch field.Type.Kind() {
55 | case reflect.Bool:
56 | v, err := key.Bool()
57 | if err != nil {
58 | log.Errorf("cannot resolve ini field %s err(%v)", opt, err)
59 | }
60 | val.Field(i).SetBool(v)
61 | case reflect.String:
62 | val.Field(i).SetString(key.String())
63 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int8:
64 | if v, err := key.Int64(); err != nil {
65 | log.Errorf("cannot convert to int64 type (%s)", err)
66 | } else {
67 | val.Field(i).SetInt(v)
68 | }
69 | }
70 | }
71 |
72 | if c.BoardcastAddr == "" {
73 | _, port, _ := net.SplitHostPort(c.ListenAddr)
74 | c.BoardcastAddr = util.InternalIP() + ":" + port
75 | }
76 |
77 | return nil
78 | }
79 |
80 | func NewConfig() *Config {
81 | return &Config{
82 | LogLevel: "warn",
83 | VerboseJobLog: true,
84 | ListenAddr: "127.0.0.1:20001",
85 | AdminAddr: "127.0.0.1:20003",
86 | LogPath: "./logs",
87 | AutoCleanTaskLog: true,
88 | NodeName: util.GetHostname(),
89 | CfgPath: "./jiacrontabd.ini",
90 | DriverName: "sqlite3",
91 | DSN: "data/jiacrontabd.db",
92 | ClientAliveInterval: 30,
93 | }
94 | }
95 |
96 | func (c *Config) loadConfig(path string) *ini.File {
97 | if !file.Exist(path) {
98 | f, err := file.CreateFile(path)
99 | if err != nil {
100 | panic(err)
101 | }
102 | f.Close()
103 | }
104 |
105 | iniFile, err := ini.Load(path)
106 | if err != nil {
107 | panic(err)
108 | }
109 | return iniFile
110 | }
111 |
--------------------------------------------------------------------------------
/jiacrontabd/const.go:
--------------------------------------------------------------------------------
1 | package jiacrontabd
2 |
--------------------------------------------------------------------------------
/jiacrontabd/daemon.go:
--------------------------------------------------------------------------------
1 | package jiacrontabd
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "jiacrontab/models"
8 | "jiacrontab/pkg/proto"
9 | "path/filepath"
10 | "strings"
11 | "sync"
12 | "time"
13 |
14 | "github.com/iwannay/log"
15 | )
16 |
17 | type ApiNotifyArgs struct {
18 | JobName string
19 | JobID uint
20 | NodeAddr string
21 | CreateUsername string
22 | CreatedAt time.Time
23 | NotifyType string
24 | }
25 |
26 | type daemonJob struct {
27 | job *models.DaemonJob
28 | daemon *Daemon
29 | ctx context.Context
30 | cancel context.CancelFunc
31 | processNum int
32 | }
33 |
34 | func (d *daemonJob) do(ctx context.Context) {
35 |
36 | d.processNum = 1
37 | t := time.NewTicker(1 * time.Second)
38 | defer t.Stop()
39 | d.daemon.wait.Add(1)
40 | cfg := d.daemon.jd.getOpts()
41 | retryNum := d.job.RetryNum
42 |
43 | defer func() {
44 | if err := recover(); err != nil {
45 | log.Errorf("%s exec panic %s \n", d.job.Name, err)
46 | }
47 | d.processNum = 0
48 | if err := models.DB().Model(d.job).Update("status", models.StatusJobStop).Error; err != nil {
49 | log.Error(err)
50 | }
51 |
52 | d.daemon.wait.Done()
53 |
54 | }()
55 |
56 | if err := models.DB().Model(d.job).Updates(map[string]interface{}{
57 | "start_at": time.Now(),
58 | "status": models.StatusJobRunning}).Error; err != nil {
59 | log.Error(err)
60 | }
61 |
62 | for {
63 |
64 | var (
65 | stop bool
66 | err error
67 | )
68 | arg := d.job.Command
69 | if d.job.Code != "" {
70 | arg = append(arg, d.job.Code)
71 | }
72 | myCmdUint := cmdUint{
73 | ctx: ctx,
74 | args: [][]string{arg},
75 | env: d.job.WorkEnv,
76 | ip: d.job.WorkIp,
77 | dir: d.job.WorkDir,
78 | user: d.job.WorkUser,
79 | label: d.job.Name,
80 | jd: d.daemon.jd,
81 | id: d.job.ID,
82 | logDir: filepath.Join(cfg.LogPath, "daemon_job"),
83 | }
84 |
85 | log.Info("exec daemon job, jobName:", d.job.Name, " jobID", d.job.ID)
86 |
87 | err = myCmdUint.launch()
88 | retryNum--
89 | d.handleNotify(err)
90 |
91 | select {
92 | case <-ctx.Done():
93 | stop = true
94 | case <-t.C:
95 | }
96 |
97 | if stop || d.job.FailRestart == false || (d.job.RetryNum > 0 && retryNum == 0) {
98 | break
99 | }
100 |
101 | if err = d.syncJob(); err != nil {
102 | break
103 | }
104 |
105 | }
106 | t.Stop()
107 |
108 | d.daemon.PopJob(d.job.ID)
109 |
110 | log.Info("daemon task end", d.job.Name)
111 | }
112 |
113 | func (d *daemonJob) syncJob() error {
114 | return models.DB().Take(d.job, "id=? and status=?", d.job.ID, models.StatusJobRunning).Error
115 | }
116 |
117 | func (d *daemonJob) handleNotify(err error) {
118 | if err == nil {
119 | return
120 | }
121 |
122 | var reply bool
123 | cfg := d.daemon.jd.getOpts()
124 | if d.job.ErrorMailNotify && len(d.job.MailTo) > 0 {
125 | var reply bool
126 | err := d.daemon.jd.rpcCallCtx(d.ctx, "Srv.SendMail", proto.SendMail{
127 | MailTo: d.job.MailTo,
128 | Subject: cfg.BoardcastAddr + "提醒常驻脚本异常退出",
129 | Content: fmt.Sprintf(
130 | "任务名:%s
创建者:%s
开始时间:%s
异常:%s",
131 | d.job.Name, d.job.CreatedUsername, time.Now().Format(proto.DefaultTimeLayout), err),
132 | }, &reply)
133 | if err != nil {
134 | log.Error("Srv.SendMail error:", err, "server addr:", cfg.AdminAddr)
135 | }
136 | }
137 |
138 | if d.job.ErrorAPINotify && len(d.job.APITo) > 0 {
139 | postData, err := json.Marshal(ApiNotifyArgs{
140 | JobName: d.job.Name,
141 | JobID: d.job.ID,
142 | CreateUsername: d.job.CreatedUsername,
143 | CreatedAt: d.job.CreatedAt,
144 | NodeAddr: cfg.BoardcastAddr,
145 | NotifyType: "error",
146 | })
147 | if err != nil {
148 | log.Error("json.Marshal error:", err)
149 | }
150 | err = d.daemon.jd.rpcCallCtx(d.ctx, "Srv.ApiPost", proto.ApiPost{
151 | Urls: d.job.APITo,
152 | Data: string(postData),
153 | }, &reply)
154 |
155 | if err != nil {
156 | log.Error("Logic.ApiPost error:", err, "server addr:", cfg.AdminAddr)
157 | }
158 | }
159 |
160 | // 钉钉webhook通知
161 | if d.job.ErrorDingdingNotify && len(d.job.DingdingTo) > 0 {
162 | nodeAddr := cfg.BoardcastAddr
163 | title := nodeAddr + "告警:常驻脚本异常退出"
164 | 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"))
165 | notifyBody := fmt.Sprintf(
166 | `{
167 | "msgtype": "markdown",
168 | "markdown": {
169 | "title": "%s",
170 | "text": "%s"
171 | }
172 | }`, title, notifyContent)
173 | err = d.daemon.jd.rpcCallCtx(d.ctx, "Srv.ApiPost", proto.ApiPost{
174 | Urls: d.job.DingdingTo,
175 | Data: notifyBody,
176 | }, &reply)
177 |
178 | if err != nil {
179 | log.Error("Logic.ApiPost error:", err, "server addr:", cfg.AdminAddr)
180 | }
181 | }
182 | }
183 |
184 | type Daemon struct {
185 | taskChannel chan *daemonJob
186 | taskMap map[uint]*daemonJob
187 | jd *Jiacrontabd
188 | lock sync.Mutex
189 | wait sync.WaitGroup
190 | }
191 |
192 | func newDaemon(taskChannelLength int, jd *Jiacrontabd) *Daemon {
193 | return &Daemon{
194 | taskMap: make(map[uint]*daemonJob),
195 | taskChannel: make(chan *daemonJob, taskChannelLength),
196 | jd: jd,
197 | }
198 | }
199 |
200 | func (d *Daemon) add(t *daemonJob) {
201 | if t != nil {
202 | if len(t.job.WorkIp) > 0 && !checkIpInWhiteList(strings.Join(t.job.WorkIp, ",")) {
203 | if err := models.DB().Model(t.job).Updates(map[string]interface{}{
204 | "status": models.StatusJobStop,
205 | //"next_exec_time": time.Time{},
206 | //"last_exit_status": "IP受限制",
207 | }).Error; err != nil {
208 | log.Error(err)
209 | }
210 | return
211 | }
212 |
213 | log.Debugf("daemon.add(%s)\n", t.job.Name)
214 | t.daemon = d
215 | d.taskChannel <- t
216 | }
217 | }
218 |
219 | // PopJob 删除调度列表中的任务
220 | func (d *Daemon) PopJob(jobID uint) {
221 | d.lock.Lock()
222 | t := d.taskMap[jobID]
223 | if t != nil {
224 | delete(d.taskMap, jobID)
225 | d.lock.Unlock()
226 | t.cancel()
227 | } else {
228 | d.lock.Unlock()
229 | }
230 | }
231 |
232 | func (d *Daemon) run() {
233 | var jobList []models.DaemonJob
234 | err := models.DB().Where("status=?", models.StatusJobRunning).Find(&jobList).Error
235 | if err != nil {
236 | log.Error("init daemon task error:", err)
237 | }
238 |
239 | for _, v := range jobList {
240 | job := v
241 | d.add(&daemonJob{
242 | job: &job,
243 | })
244 | }
245 |
246 | d.process()
247 | }
248 |
249 | func (d *Daemon) process() {
250 | go func() {
251 | for v := range d.taskChannel {
252 | d.lock.Lock()
253 | if t := d.taskMap[v.job.ID]; t == nil {
254 | d.taskMap[v.job.ID] = v
255 | d.lock.Unlock()
256 | v.ctx, v.cancel = context.WithCancel(context.Background())
257 | go v.do(v.ctx)
258 | } else {
259 | d.lock.Unlock()
260 | }
261 | }
262 | }()
263 | }
264 |
265 | func (d *Daemon) count() int {
266 | var count int
267 | d.lock.Lock()
268 | count = len(d.taskMap)
269 | d.lock.Unlock()
270 | return count
271 | }
272 |
273 | func (d *Daemon) waitDone() {
274 | d.wait.Wait()
275 | }
276 |
--------------------------------------------------------------------------------
/jiacrontabd/dependencies.go:
--------------------------------------------------------------------------------
1 | package jiacrontabd
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "jiacrontab/pkg/proto"
7 | "time"
8 |
9 | "github.com/iwannay/log"
10 | )
11 |
12 | type depEntry struct {
13 | jobID uint // 定时任务id
14 | jobUniqueID string // job的唯一标志
15 | processID int // 当前依赖的父级任务(可能存在多个并发的task)
16 | id string // depID uuid
17 | once bool
18 | workDir string
19 | user string
20 | env []string
21 | from string
22 | commands []string
23 | dest string
24 | done bool
25 | timeout int64
26 | err error
27 | ctx context.Context
28 | name string
29 | logPath string
30 | logContent []byte
31 | }
32 |
33 | func newDependencies(jd *Jiacrontabd) *dependencies {
34 | return &dependencies{
35 | jd: jd,
36 | dep: make(chan *depEntry, 100),
37 | }
38 | }
39 |
40 | type dependencies struct {
41 | jd *Jiacrontabd
42 | dep chan *depEntry
43 | }
44 |
45 | func (d *dependencies) add(t *depEntry) {
46 | select {
47 | case d.dep <- t:
48 | default:
49 | log.Warnf("discard %v", t)
50 | }
51 |
52 | }
53 |
54 | func (d *dependencies) run() {
55 | go func() {
56 | for {
57 | select {
58 | case t := <-d.dep:
59 | go d.exec(t)
60 | }
61 | }
62 | }()
63 | }
64 |
65 | // TODO: 主任务退出杀死依赖
66 | func (d *dependencies) exec(task *depEntry) {
67 |
68 | var (
69 | reply bool
70 | err error
71 | )
72 |
73 | if task.timeout == 0 {
74 | // 默认超时10分钟
75 | task.timeout = 600
76 | }
77 |
78 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(task.timeout)*time.Second)
79 | defer cancel()
80 | myCmdUnit := cmdUint{
81 | args: [][]string{task.commands},
82 | ctx: ctx,
83 | dir: task.workDir,
84 | user: task.user,
85 | logPath: task.logPath,
86 | ignoreFileLog: true,
87 | jd: d.jd,
88 | exportLog: true,
89 | }
90 |
91 | log.Infof("dep start exec %s->%v", task.name, task.commands)
92 | task.err = myCmdUnit.launch()
93 | task.logContent = bytes.TrimRight(myCmdUnit.content, "\x00")
94 | task.done = true
95 | log.Infof("exec %s %s cost %.4fs %v", task.name, task.commands, float64(myCmdUnit.costTime)/1000000000, err)
96 |
97 | task.dest, task.from = task.from, task.dest
98 |
99 | if !d.jd.SetDependDone(task) {
100 | err = d.jd.rpcCallCtx(ctx, "Srv.SetDependDone", proto.DepJob{
101 | Name: task.name,
102 | Dest: task.dest,
103 | From: task.from,
104 | ID: task.id,
105 | JobUniqueID: task.jobUniqueID,
106 | ProcessID: task.processID,
107 | JobID: task.jobID,
108 | Commands: task.commands,
109 | LogContent: task.logContent,
110 | Err: err,
111 | Timeout: task.timeout,
112 | }, &reply)
113 |
114 | if err != nil {
115 | log.Error("Srv.SetDependDone error:", err, "server addr:", d.jd.getOpts().AdminAddr)
116 | }
117 |
118 | if !reply {
119 | log.Errorf("task %s %v call Srv.SetDependDone failed! err:%v", task.name, task.commands, err)
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/jiacrontabd/util.go:
--------------------------------------------------------------------------------
1 | package jiacrontabd
2 |
3 | import (
4 | "jiacrontab/pkg/util"
5 | "os"
6 |
7 | "github.com/iwannay/log"
8 |
9 | "container/list"
10 | "net"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | func writeFile(fPath string, content *[]byte) {
16 | f, err := util.TryOpen(fPath, os.O_APPEND|os.O_CREATE|os.O_RDWR)
17 | if err != nil {
18 | log.Errorf("writeLog: %v", err)
19 | return
20 | }
21 | defer f.Close()
22 | f.Write(*content)
23 | }
24 |
25 | func GetIntranetIpList() *list.List {
26 | ipList := list.New()
27 | addrs, err := net.InterfaceAddrs()
28 |
29 | if err != nil {
30 | return ipList
31 | }
32 |
33 | for _, address := range addrs {
34 | // 检查ip地址判断是否回环地址
35 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
36 | if ipnet.IP.To4() != nil {
37 | ipList.PushBack(ipnet.IP.String())
38 | }
39 |
40 | }
41 | }
42 | return ipList
43 | }
44 |
45 | func isIpBelong(ip, cidr string) bool {
46 | ipAddr := strings.Split(ip, `.`)
47 | if len(ipAddr) < 4 {
48 | return false
49 | }
50 | if ip == cidr {
51 | return true
52 | }
53 | cidrArr := strings.Split(cidr, `/`)
54 | if len(cidrArr) < 2 {
55 | return false
56 | }
57 | var tmp = make([]string, 0)
58 | for key, value := range strings.Split(`255.255.255.0`, `.`) {
59 | iint, _ := strconv.Atoi(value)
60 |
61 | iint2, _ := strconv.Atoi(ipAddr[key])
62 |
63 | tmp = append(tmp, strconv.Itoa(iint&iint2))
64 | }
65 | return strings.Join(tmp, `.`) == cidrArr[0]
66 | }
67 |
68 | func checkIpInWhiteList(whiteIpStr string) bool {
69 | myIps := GetIntranetIpList()
70 | whiteIpList := strings.Split(whiteIpStr, `,`)
71 | if len(whiteIpList) == 0 {
72 | return true
73 | }
74 | for item := myIps.Front(); nil != item; item = item.Next() {
75 | for i := range whiteIpList {
76 | isBelong := isIpBelong(item.Value.(string), whiteIpList[i])
77 | if isBelong {
78 | return true
79 | }
80 | }
81 | }
82 | return false
83 | }
84 |
--------------------------------------------------------------------------------
/models/crontab.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "database/sql/driver"
5 | "encoding/json"
6 | "errors"
7 | "jiacrontab/pkg/util"
8 | "time"
9 |
10 | "gorm.io/gorm"
11 | )
12 |
13 | // JobStatus 任务状态
14 | type JobStatus int
15 |
16 | const (
17 | // StatusJobUnaudited 未审核
18 | StatusJobUnaudited JobStatus = 0
19 | // StatusJobOk 等待调度
20 | StatusJobOk JobStatus = 1
21 | // StatusJobTiming 定时中
22 | StatusJobTiming JobStatus = 2
23 | // StatusJobRunning 执行中
24 | StatusJobRunning JobStatus = 3
25 | // StatusJobStop 已停止
26 | StatusJobStop JobStatus = 4
27 | )
28 |
29 | type CrontabJob struct {
30 | gorm.Model
31 | Name string `json:"name" gorm:"index;not null"`
32 | GroupID uint `json:"groupID" grom:"index"`
33 | Command StringSlice `json:"command" gorm:"type:varchar(1000)"`
34 | Code string `json:"code" gorm:"type:TEXT"`
35 | DependJobs DependJobs `json:"dependJobs" gorm:"type:TEXT"`
36 | LastCostTime float64 `json:"lastCostTime"`
37 | LastExecTime time.Time `json:"lastExecTime"`
38 | NextExecTime time.Time `json:"nextExecTime"`
39 | Failed bool `json:"failed"`
40 | LastExitStatus string `json:"lastExitStatus" grom:"index"`
41 | CreatedUserID uint `json:"createdUserId"`
42 | CreatedUsername string `json:"createdUsername"`
43 | UpdatedUserID uint `json:"updatedUserID"`
44 | UpdatedUsername string `json:"updatedUsername"`
45 | WorkUser string `json:"workUser"`
46 | WorkIp StringSlice `json:"workIp" gorm:"type:varchar(1000)"`
47 | WorkEnv StringSlice `json:"workEnv" gorm:"type:varchar(1000)"`
48 | WorkDir string `json:"workDir"`
49 | KillChildProcess bool `json:"killChildProcess"`
50 | Timeout int `json:"timeout"`
51 | ProcessNum int `json:"processNum"`
52 | ErrorMailNotify bool `json:"errorMailNotify"`
53 | ErrorAPINotify bool `json:"errorAPINotify"`
54 | ErrorDingdingNotify bool `json:"errorDingdingNotify"`
55 | RetryNum int `json:"retryNum"`
56 | Status JobStatus `json:"status"`
57 | IsSync bool `json:"isSync"` // 脚本是否同步执行
58 | MailTo StringSlice `json:"mailTo" gorm:"type:varchar(1000)"`
59 | APITo StringSlice `json:"APITo" gorm:"type:varchar(1000)"`
60 | DingdingTo StringSlice `json:"DingdingTo" gorm:"type:varchar(1000)"`
61 | MaxConcurrent uint `json:"maxConcurrent"` // 脚本最大并发量
62 | TimeoutTrigger StringSlice `json:"timeoutTrigger" gorm:"type:varchar(20)"`
63 | TimeArgs TimeArgs `json:"timeArgs" gorm:"type:TEXT"`
64 | }
65 |
66 | type StringSlice []string
67 |
68 | func (s *StringSlice) Scan(v interface{}) error {
69 |
70 | switch val := v.(type) {
71 | case string:
72 | return json.Unmarshal([]byte(val), s)
73 | case []byte:
74 | return json.Unmarshal(val, s)
75 | default:
76 | return errors.New("not support")
77 | }
78 | }
79 |
80 | func (s StringSlice) MarshalJSON() ([]byte, error) {
81 | if s == nil {
82 | s = make(StringSlice, 0)
83 | }
84 | return json.Marshal([]string(s))
85 | }
86 |
87 | func (s StringSlice) Value() (driver.Value, error) {
88 | if s == nil {
89 | s = make(StringSlice, 0)
90 | }
91 | bts, err := json.Marshal(s)
92 | return string(bts), err
93 | }
94 |
95 | type DependJobs []DependJob
96 |
97 | func (d *DependJobs) Scan(v interface{}) error {
98 | switch val := v.(type) {
99 | case string:
100 | return json.Unmarshal([]byte(val), d)
101 | case []byte:
102 | return json.Unmarshal(val, d)
103 | default:
104 | return errors.New("not support")
105 | }
106 |
107 | }
108 |
109 | func (d DependJobs) Value() (driver.Value, error) {
110 |
111 | if d == nil {
112 | d = make(DependJobs, 0)
113 | }
114 |
115 | for k, _ := range d {
116 | d[k].ID = util.UUID()
117 | }
118 |
119 | bts, err := json.Marshal(d)
120 | return string(bts), err
121 | }
122 |
123 | func (d DependJobs) MarshalJSON() ([]byte, error) {
124 | if d == nil {
125 | d = make(DependJobs, 0)
126 | }
127 | type m DependJobs
128 | return json.Marshal(m(d))
129 | }
130 |
131 | type TimeArgs struct {
132 | Weekday string `json:"weekday"`
133 | Month string `json:"month"`
134 | Day string `json:"day"`
135 | Hour string `json:"hour"`
136 | Minute string `json:"minute"`
137 | Second string `json:"second"`
138 | }
139 |
140 | func (c *TimeArgs) Scan(v interface{}) error {
141 | switch val := v.(type) {
142 | case string:
143 | return json.Unmarshal([]byte(val), c)
144 | case []byte:
145 | return json.Unmarshal(val, c)
146 | default:
147 | return errors.New("not support")
148 | }
149 |
150 | }
151 |
152 | func (c TimeArgs) Value() (driver.Value, error) {
153 | bts, err := json.Marshal(c)
154 | return string(bts), err
155 | }
156 |
157 | type DependJob struct {
158 | Dest string `json:"dest"`
159 | From string `json:"from"`
160 | JobID uint `json:"jobID"`
161 | ID string `json:"id"`
162 | WorkUser string `json:"user"`
163 | WorkDir string `json:"workDir"`
164 | Command []string `json:"command"`
165 | Code string `json:"code"`
166 | Timeout int64 `json:"timeout"`
167 | }
168 |
169 | type PipeComamnds [][]string
170 |
171 | func (p *PipeComamnds) Scan(v interface{}) error {
172 | switch val := v.(type) {
173 | case string:
174 | return json.Unmarshal([]byte(val), p)
175 | case []byte:
176 | return json.Unmarshal(val, p)
177 | default:
178 | return errors.New("not support")
179 | }
180 |
181 | }
182 |
183 | func (p PipeComamnds) Value() (driver.Value, error) {
184 | if p == nil {
185 | p = make(PipeComamnds, 0)
186 | }
187 | bts, err := json.Marshal(p)
188 | return string(bts), err
189 | }
190 |
191 | func (d PipeComamnds) MarshalJSON() ([]byte, error) {
192 | if d == nil {
193 | d = make(PipeComamnds, 0)
194 | }
195 | type m PipeComamnds
196 | return json.Marshal(m(d))
197 | }
198 |
199 | type CrontabArgs struct {
200 | Weekday string
201 | Month string
202 | Day string
203 | Hour string
204 | Minute string
205 | }
206 |
207 | func (c *CrontabArgs) Scan(v interface{}) error {
208 | switch val := v.(type) {
209 | case string:
210 | return json.Unmarshal([]byte(val), c)
211 | case []byte:
212 | return json.Unmarshal(val, c)
213 | default:
214 | return errors.New("not support")
215 | }
216 |
217 | }
218 |
219 | func (c CrontabArgs) Value() (driver.Value, error) {
220 | bts, err := json.Marshal(c)
221 | return string(bts), err
222 | }
223 |
--------------------------------------------------------------------------------
/models/crontab_test.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestStringSlice_Value(t *testing.T) {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/models/daemon.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type DaemonJob struct {
10 | gorm.Model
11 | Name string `json:"name" gorm:"index;not null"`
12 | GroupID uint `json:"groupID" grom:"index"`
13 | Command StringSlice `json:"command" gorm:"type:varchar(1000)"`
14 | Code string `json:"code" gorm:"type:TEXT"`
15 | ErrorMailNotify bool `json:"errorMailNotify"`
16 | ErrorAPINotify bool `json:"errorAPINotify"`
17 | ErrorDingdingNotify bool `json:"errorDingdingNotify"`
18 | Status JobStatus `json:"status"`
19 | MailTo StringSlice `json:"mailTo" gorm:"type:varchar(1000)"`
20 | APITo StringSlice `json:"APITo" gorm:"type:varchar(1000)"`
21 | DingdingTo StringSlice `json:"DingdingTo" gorm:"type:varchar(1000)"`
22 | FailRestart bool `json:"failRestart"`
23 | RetryNum int `json:"retryNum"`
24 | StartAt time.Time `json:"startAt"`
25 | WorkUser string `json:"workUser"`
26 | WorkIp StringSlice `json:"workIp" gorm:"type:varchar(1000)"`
27 | WorkEnv StringSlice `json:"workEnv" gorm:"type:varchar(1000)"`
28 | WorkDir string `json:"workDir"`
29 | CreatedUserID uint `json:"createdUserId"`
30 | CreatedUsername string `json:"createdUsername"`
31 | UpdatedUserID uint `json:"updatedUserID"`
32 | UpdatedUsername string `json:"updatedUsername"`
33 | }
34 |
--------------------------------------------------------------------------------
/models/db.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/iwannay/log"
10 | "gorm.io/driver/mysql"
11 | "gorm.io/driver/postgres"
12 | "gorm.io/driver/sqlite"
13 | "gorm.io/gorm"
14 | )
15 |
16 | // D alias DB
17 | type D = gorm.DB
18 |
19 | var (
20 | db *D
21 | debugMode bool
22 | )
23 |
24 | func CreateDB(dialect string, dsn string) (err error) {
25 | switch dialect {
26 | case "sqlite3":
27 | return createSqlite(dsn)
28 | case "mysql":
29 | db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
30 | PrepareStmt: true,
31 | DisableForeignKeyConstraintWhenMigrating: true,
32 | })
33 | return
34 | case "postgres":
35 | db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
36 | PrepareStmt: true,
37 | DisableForeignKeyConstraintWhenMigrating: true,
38 | })
39 | return
40 | }
41 | return fmt.Errorf("unknow database type %s", dialect)
42 | }
43 |
44 | func createSqlite(dsn string) error {
45 | var err error
46 | if dsn == "" {
47 | return errors.New("sqlite:db file cannot empty")
48 | }
49 |
50 | dbDir := filepath.Dir(filepath.Clean(dsn))
51 | err = os.MkdirAll(dbDir, 0755)
52 | if err != nil {
53 | return fmt.Errorf("sqlite: makedir failed %s", err)
54 | }
55 | db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
56 | DisableForeignKeyConstraintWhenMigrating: true,
57 | })
58 | if err == nil {
59 | d, err := db.DB()
60 | if err != nil {
61 | panic(err)
62 | }
63 | d.SetMaxOpenConns(1)
64 | }
65 | return err
66 | }
67 |
68 | func DB() *D {
69 | if db == nil {
70 | panic("you must call CreateDb first")
71 | }
72 | if debugMode {
73 | return db.Debug()
74 | }
75 | return db
76 | }
77 |
78 | func Transactions(fn func(tx *gorm.DB) error) error {
79 | if fn == nil {
80 | return errors.New("fn is nil")
81 | }
82 | tx := DB().Begin()
83 | defer func() {
84 | if err := recover(); err != nil {
85 | DB().Rollback()
86 | }
87 | }()
88 |
89 | if fn(tx) != nil {
90 | tx.Rollback()
91 | }
92 | return tx.Commit().Error
93 | }
94 |
95 | func InitModel(driverName string, dsn string, debug bool) error {
96 | if driverName == "" || dsn == "" {
97 | return errors.New("driverName and dsn cannot empty")
98 | }
99 |
100 | if err := CreateDB(driverName, dsn); err != nil {
101 | return err
102 | }
103 | debugMode = debug
104 | AutoMigrate()
105 | return nil
106 | }
107 |
108 | func AutoMigrate() {
109 | if err := DB().AutoMigrate(&SysSetting{}, &Node{}, &Group{}, &User{}, &Event{}, &JobHistory{}); err != nil {
110 | log.Fatal(err)
111 | }
112 | if err := DB().FirstOrCreate(&SuperGroup).Error; err != nil {
113 | log.Fatal(err)
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/models/event.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/iwannay/log"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type EventSourceName string
10 | type EventSourceUsername string
11 |
12 | type Event struct {
13 | gorm.Model
14 | GroupID uint `json:"groupID" gorm:"index"`
15 | Username string `json:"username"`
16 | UserID uint `json:"userID" gorm:"index"`
17 | EventDesc string `json:"eventDesc"`
18 | TargetName string `json:"targetName"`
19 | SourceUsername string `json:"sourceUsername"`
20 | SourceName string `json:"sourceName" gorm:"index;size:500"`
21 | Content string `json:"content"`
22 | }
23 |
24 | func (e *Event) Pub() {
25 | err := DB().Model(e).Create(e).Error
26 | if err != nil {
27 | log.Error("Event.Pub", err)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/models/group.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "gorm.io/gorm"
5 | )
6 |
7 | var SuperGroup Group
8 |
9 | type Group struct {
10 | gorm.Model
11 | Name string `json:"name" gorm:"not null;uniqueIndex;size:500"`
12 | }
13 |
14 | func (g *Group) Save() error {
15 | if g.ID == 0 {
16 | return DB().Create(g).Error
17 | }
18 | return DB().Save(g).Error
19 | }
20 |
21 | func init() {
22 | SuperGroup.ID = 1
23 | SuperGroup.Name = "超级管理员"
24 | }
25 |
--------------------------------------------------------------------------------
/models/history.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/iwannay/log"
7 | "gorm.io/gorm"
8 | )
9 |
10 | const (
11 | JobTypeCrontab JobType = 0
12 | JobTypeDaemon JobType = 1
13 | )
14 |
15 | type JobType uint8
16 |
17 | type JobHistory struct {
18 | gorm.Model
19 | JobType JobType `json:"jobType"` // 0:定时任务,1:常驻任务
20 | JobID uint `json:"jobID"`
21 | JobName string `json:"jobName"`
22 | Addr string `json:"addr" gorm:"index"`
23 | ExitMsg string `json:"exitMsg"`
24 | StartTime time.Time `json:"StartTime"`
25 | EndTime time.Time `json:"endTime"`
26 | }
27 |
28 | func PushJobHistory(job *JobHistory) {
29 | err := DB().Create(job).Error
30 | if err != nil {
31 | log.Error("PushJobHistory failed:", err)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/models/node.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "errors"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Node struct {
10 | gorm.Model
11 | Name string `json:"name" gorm:"not null"`
12 | DaemonTaskNum uint `json:"daemonTaskNum"`
13 | Disabled bool `json:"disabled"` // 通信失败时Disabled会被设置为true
14 | CrontabTaskNum uint `json:"crontabTaskNum"`
15 | GroupID uint `json:"groupID" gorm:"not null;uniqueIndex:uni_group_addr" `
16 | CrontabJobAuditNum uint `json:"crontabJobAuditNum"`
17 | DaemonJobAuditNum uint `json:"daemonJobAuditNum"`
18 | CrontabJobFailNum uint `json:"crontabJobFailNum"`
19 | Addr string `json:"addr" gorm:"not null;uniqueIndex:uni_group_addr;size:100"`
20 | Group Group `json:"group"`
21 | }
22 |
23 | func (n *Node) VerifyUserGroup(userID, groupID uint, addr string) bool {
24 | var user User
25 |
26 | if groupID == SuperGroup.ID {
27 | return true
28 | }
29 |
30 | if DB().Take(&user, "id=? and group_id=?", userID, groupID).Error != nil {
31 | return false
32 | }
33 |
34 | return n.Exists(groupID, addr)
35 | }
36 |
37 | func (n *Node) Delete(groupID uint, addr string) error {
38 | var ret *gorm.DB
39 | DB().Take(n, "group_id=? and addr=?", groupID, addr)
40 | if groupID == SuperGroup.ID {
41 | // 超级管理员分组采用软删除
42 | ret = DB().Delete(n, "group_id=? and addr=?", groupID, addr)
43 | } else {
44 | ret = DB().Unscoped().Delete(n, "group_id=? and addr=?", groupID, addr)
45 | }
46 |
47 | if ret.Error != nil {
48 | return ret.Error
49 | }
50 |
51 | if ret.RowsAffected == 0 {
52 | return errors.New("Delete failed")
53 | }
54 | return nil
55 | }
56 |
57 | func (n *Node) Rename(groupID uint, addr string) error {
58 | return DB().Model(n).Where("group_id=? and addr=?", groupID, addr).Updates(n).Error
59 | }
60 |
61 | // GroupNode 为节点分组,复制groupID=1分组中node至目标分组
62 | func (n *Node) GroupNode(addr string, targetGroupID uint, targetNodeName, targetGroupName string) error {
63 |
64 | // 新建分组
65 | if targetGroupID == 0 {
66 | group := &Group{
67 | Name: targetGroupName,
68 | }
69 | if err := DB().Save(group).Error; err != nil {
70 | return err
71 | }
72 | targetGroupID = group.ID
73 | }
74 |
75 | err := DB().Preload("Group").Where("group_id=? and addr=?", SuperGroup.ID, addr).Take(n).Error
76 | if err != nil {
77 | return err
78 | }
79 |
80 | if targetNodeName == "" {
81 | targetNodeName = n.Name
82 | }
83 |
84 | return DB().Save(&Node{
85 | Addr: addr,
86 | GroupID: targetGroupID,
87 | Name: targetNodeName,
88 | }).Error
89 | }
90 |
91 | func (n *Node) Exists(groupID uint, addr string) bool {
92 | if DB().Take(n, "group_id=? and addr=?", groupID, addr).Error != nil {
93 | return false
94 | }
95 | return true
96 | }
97 |
--------------------------------------------------------------------------------
/models/setting.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type SysSetting struct {
10 | gorm.Model
11 | Class int `json:"class"` // 设置分类,1 Ldap配置
12 | Content json.RawMessage `json:"content" gorm:"column:content; type:json"` // 配置内容
13 | }
14 |
--------------------------------------------------------------------------------
/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "crypto/md5"
5 | "errors"
6 | "fmt"
7 | "jiacrontab/pkg/util"
8 | "time"
9 |
10 | "github.com/iwannay/log"
11 |
12 | "gorm.io/gorm"
13 | )
14 |
15 | type User struct {
16 | gorm.Model
17 | Username string `json:"username" gorm:"not null;uniqueIndex;size:500"`
18 | Passwd string `json:"-"`
19 | Salt string `json:"-"`
20 | Avatar string `json:"avatar"`
21 | Version int64 `json:"version"`
22 | Gender string `json:"gender"`
23 | GroupID uint `json:"groupID" grom:"index"`
24 | Root bool `json:"root"`
25 | Mail string `json:"mail"`
26 | Group Group `json:"group"`
27 | }
28 |
29 | func (u *User) getSalt() string {
30 | var (
31 | seed = "1234567890!@#$%^&*()ABCDEFGHIJK"
32 | salt [10]byte
33 | )
34 | for k := range salt {
35 | salt[k] = seed[util.RandIntn(len(seed))]
36 | }
37 |
38 | return string(salt[0:10])
39 | }
40 |
41 | // Verify 验证用户
42 | func (u *User) Verify(username, passwd string) bool {
43 | ret := DB().Take(u, "username=?", username)
44 |
45 | if ret.Error != nil {
46 | log.Error("user.Verify:", ret.Error)
47 | return false
48 | }
49 |
50 | bts := md5.Sum([]byte(fmt.Sprint(passwd, u.Salt)))
51 | return fmt.Sprintf("%x", bts) == u.Passwd
52 | }
53 |
54 | // Verify 验证用户
55 | func (u *User) VerifyByUserId(id uint, passwd string) bool {
56 | ret := DB().Take(u, "id=?", id)
57 |
58 | if ret.Error != nil {
59 | log.Error("user.Verify:", ret.Error)
60 | return false
61 | }
62 |
63 | bts := md5.Sum([]byte(fmt.Sprint(passwd, u.Salt)))
64 | return fmt.Sprintf("%x", bts) == u.Passwd
65 | }
66 |
67 | func (u *User) setPasswd() {
68 | if u.Passwd == "" {
69 | return
70 | }
71 | u.Salt = u.getSalt()
72 | bts := md5.Sum([]byte(fmt.Sprint(u.Passwd, u.Salt)))
73 | u.Passwd = fmt.Sprintf("%x", bts)
74 | }
75 |
76 | func (u *User) Create() error {
77 | u.setPasswd()
78 | u.Version = time.Now().Unix()
79 | return DB().Create(u).Error
80 | }
81 |
82 | func (u User) Update() error {
83 | u.setPasswd()
84 | u.Version = time.Now().Unix()
85 | if u.ID == 0 && u.Username != "" {
86 | return DB().Where("username=?", u.Username).Updates(u).Error
87 | }
88 | return DB().Model(&u).Updates(u).Error
89 | }
90 |
91 | func (u *User) Delete() error {
92 | if err := DB().Take(u, "id=?", u.ID).Error; err != nil {
93 | return err
94 | }
95 | return DB().Delete(u).Error
96 | }
97 |
98 | func (u *User) SetGroup(group *Group) error {
99 |
100 | if u.GroupID != 0 {
101 | if err := DB().Take(group, "id=?", u.GroupID).Error; err != nil {
102 | return fmt.Errorf("查询分组失败:%s", err)
103 | }
104 | }
105 | if u.ID == 1 {
106 | return errors.New("系统用户不允许修改")
107 | }
108 |
109 | defer DB().Take(u, "id=?", u.ID)
110 |
111 | return DB().Model(u).Where("id=?", u.ID).Updates(map[string]interface{}{
112 | "group_id": u.GroupID,
113 | "version": time.Now().Unix(),
114 | "root": u.Root,
115 | }).Error
116 | }
117 |
--------------------------------------------------------------------------------
/pkg/base/stat.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 | "strings"
7 | "sync"
8 | "sync/atomic"
9 | "time"
10 | )
11 |
12 | const (
13 | minuteTimeLayout = "200601021504"
14 | dateTimeLayout = "2006-01-02 15:04:05"
15 | defaultReserveMinutes = 60
16 | defaultCheckTimeMinutes = 10
17 | )
18 |
19 | // Stat 应用内统计
20 | var Stat *stat
21 |
22 | type (
23 | stat struct {
24 | ServerStartTime time.Time
25 | EnableDetailRequestData bool
26 | TotalRequestCount uint64
27 |
28 | IntervalRequestData *Storage
29 | DetailRequestURLData *Storage
30 | TotalErrorCount uint64
31 | IntervalErrorData *Storage
32 | DetailErrorPageData *Storage
33 | DetailErrorData *Storage
34 | DetailHTTPCodeData *Storage
35 |
36 | dataChanRequest chan *RequestInfo
37 | dataChanError chan *ErrorInfo
38 | dataChanHTTPCode chan *HttpCodeInfo
39 | TotalConcurrentCount int64
40 |
41 | infoPool *pool
42 | }
43 |
44 | pool struct {
45 | requestInfo sync.Pool
46 | errorInfo sync.Pool
47 | httpCodeInfo sync.Pool
48 | }
49 |
50 | // RequestInfo 请求url信息
51 | RequestInfo struct {
52 | URL string
53 | Code int
54 | Num uint64
55 | }
56 |
57 | ErrorInfo struct {
58 | URL string
59 | ErrMsg string
60 | Num uint64
61 | }
62 |
63 | HttpCodeInfo struct {
64 | URL string
65 | Code int
66 | Num uint64
67 | }
68 | )
69 |
70 | func (s *stat) QueryIntervalRequstData(key string) uint64 {
71 | val, _ := s.IntervalRequestData.GetUint64(key)
72 | return val
73 | }
74 |
75 | func (s *stat) QueryIntervalErrorData(key string) uint64 {
76 | val, _ := s.IntervalErrorData.GetUint64(key)
77 | return val
78 | }
79 |
80 | func (s *stat) AddRequestCount(page string, code int, num uint64) uint64 {
81 |
82 | if !strings.HasPrefix(page, "/debug") {
83 | atomic.AddUint64(&s.TotalRequestCount, num)
84 | s.addRequestData(page, code, num)
85 | s.addHTTPCodeData(page, code, num)
86 | }
87 | atomic.AddInt64(&s.TotalConcurrentCount, -1)
88 | return atomic.LoadUint64(&s.TotalRequestCount)
89 | }
90 |
91 | func (s *stat) AddConcurrentCount() {
92 | atomic.AddInt64(&s.TotalConcurrentCount, 1)
93 | }
94 |
95 | func (s *stat) AddErrorCount(page string, err error, num uint64) uint64 {
96 | atomic.AddUint64(&s.TotalErrorCount, num)
97 | s.addErrorData(page, err, num)
98 | return atomic.LoadUint64(&s.TotalErrorCount)
99 | }
100 |
101 | func (s *stat) addRequestData(page string, code int, num uint64) {
102 | info := s.infoPool.requestInfo.Get().(*RequestInfo)
103 | info.URL = page
104 | info.Code = code
105 | info.Num = num
106 | s.dataChanRequest <- info
107 | }
108 |
109 | func (s *stat) addErrorData(page string, err error, num uint64) {
110 | info := s.infoPool.errorInfo.Get().(*ErrorInfo)
111 | info.URL = page
112 | info.ErrMsg = err.Error()
113 | info.Num = num
114 | s.dataChanError <- info
115 | }
116 |
117 | func (s *stat) addHTTPCodeData(page string, code int, num uint64) {
118 | info := s.infoPool.httpCodeInfo.Get().(*HttpCodeInfo)
119 | info.URL = page
120 | info.Code = code
121 | info.Num = num
122 | s.dataChanHTTPCode <- info
123 | }
124 |
125 | func (s *stat) handleInfo() {
126 | for {
127 | select {
128 | case info := <-s.dataChanRequest:
129 | {
130 | if s.EnableDetailRequestData {
131 | if info.Code != http.StatusNotFound {
132 | key := strings.ToLower(info.URL)
133 | val, _ := s.DetailRequestURLData.GetUint64(key)
134 | s.DetailRequestURLData.Store(key, val+info.Num)
135 | }
136 | }
137 |
138 | key := time.Now().Format(minuteTimeLayout)
139 | val, _ := s.IntervalRequestData.GetUint64(key)
140 | s.IntervalRequestData.Store(key, val+info.Num)
141 |
142 | s.infoPool.requestInfo.Put(info)
143 | }
144 | case info := <-s.dataChanError:
145 | {
146 | key := strings.ToLower(info.URL)
147 | val, _ := s.DetailErrorPageData.GetUint64(key)
148 | s.DetailErrorPageData.Store(key, val+info.Num)
149 |
150 | key = info.ErrMsg
151 |
152 | val, _ = s.DetailErrorData.GetUint64(key)
153 |
154 | s.DetailErrorData.Store(key, val+info.Num)
155 |
156 | key = time.Now().Format(minuteTimeLayout)
157 | val, _ = s.IntervalErrorData.GetUint64(key)
158 | s.IntervalErrorData.Store(key, val+info.Num)
159 |
160 | s.infoPool.errorInfo.Put(info)
161 |
162 | }
163 |
164 | case info := <-s.dataChanHTTPCode:
165 | {
166 | key := strconv.Itoa(info.Code)
167 | val, _ := s.DetailHTTPCodeData.GetUint64(key)
168 | s.DetailHTTPCodeData.Store(key, val+info.Num)
169 |
170 | s.infoPool.httpCodeInfo.Put(info)
171 | }
172 | }
173 | }
174 | }
175 |
176 | func (s *stat) Collect() map[string]interface{} {
177 | var dataMap = make(map[string]interface{})
178 | dataMap["ServerStartTime"] = s.ServerStartTime.Format(dateTimeLayout)
179 | dataMap["TotalRequestCount"] = atomic.LoadUint64(&s.TotalRequestCount)
180 | dataMap["TotalConcurrentCount"] = atomic.LoadInt64(&s.TotalConcurrentCount)
181 | dataMap["TotalErrorCount"] = s.TotalErrorCount
182 | dataMap["IntervalRequestData"] = s.IntervalRequestData.All()
183 | dataMap["DetailRequestUrlData"] = s.DetailRequestURLData.All()
184 | dataMap["IntervalErrorData"] = s.IntervalErrorData.All()
185 | dataMap["DetailErrorPageData"] = s.DetailErrorPageData.All()
186 | dataMap["DetailErrorData"] = s.DetailErrorData.All()
187 | dataMap["DetailHttpCodeData"] = s.DetailHTTPCodeData.All()
188 | return dataMap
189 | }
190 |
191 | func (s *stat) gc() {
192 | var needRemoveKey []string
193 | now, _ := time.Parse(minuteTimeLayout, time.Now().Format(minuteTimeLayout))
194 |
195 | if s.IntervalRequestData.Len() > defaultReserveMinutes {
196 | s.IntervalRequestData.Range(func(key, val interface{}) bool {
197 | keyString := key.(string)
198 | if t, err := time.Parse(minuteTimeLayout, keyString); err != nil {
199 | needRemoveKey = append(needRemoveKey, keyString)
200 | } else {
201 | if now.Sub(t) > (defaultReserveMinutes * time.Minute) {
202 | needRemoveKey = append(needRemoveKey, keyString)
203 | }
204 | }
205 | return true
206 | })
207 | }
208 |
209 | for _, v := range needRemoveKey {
210 | s.IntervalRequestData.Delete(v)
211 | }
212 |
213 | needRemoveKey = []string{}
214 | if s.IntervalErrorData.Len() > defaultReserveMinutes {
215 | s.IntervalErrorData.Range(func(key, val interface{}) bool {
216 | keyString := key.(string)
217 | if t, err := time.Parse(minuteTimeLayout, keyString); err != nil {
218 | needRemoveKey = append(needRemoveKey, keyString)
219 | } else {
220 | if now.Sub(t) > defaultReserveMinutes*time.Minute {
221 | needRemoveKey = append(needRemoveKey, keyString)
222 | }
223 | }
224 | return true
225 | })
226 |
227 | }
228 |
229 | for _, v := range needRemoveKey {
230 | s.IntervalErrorData.Delete(v)
231 | }
232 |
233 | time.AfterFunc(time.Duration(defaultCheckTimeMinutes)*time.Minute, s.gc)
234 |
235 | }
236 |
237 | func init() {
238 | Stat = &stat{
239 | // 服务启动时间
240 | ServerStartTime: time.Now(),
241 | // 单位时间内请求数据 - 分钟为单位
242 | IntervalRequestData: NewStorage(),
243 | // 明细请求页面数据 - 以不带参数的访问url为key
244 | DetailRequestURLData: NewStorage(),
245 | // 单位时间内异常次数 - 按分钟为单位
246 | IntervalErrorData: NewStorage(),
247 | // 明细异常页面数据 - 以不带参数的访问url为key
248 | DetailErrorPageData: NewStorage(),
249 | // 单位时间内异常次数 - 按分钟为单位
250 | DetailErrorData: NewStorage(),
251 | // 明细Http状态码数据 - 以HttpCode为key,例如200、500等
252 | DetailHTTPCodeData: NewStorage(),
253 | dataChanRequest: make(chan *RequestInfo, 1000),
254 | dataChanError: make(chan *ErrorInfo, 1000),
255 | dataChanHTTPCode: make(chan *HttpCodeInfo, 1000),
256 | EnableDetailRequestData: true, //是否启用详细请求数据统计, 当url较多时,导致内存占用过大
257 | infoPool: &pool{
258 | requestInfo: sync.Pool{
259 | New: func() interface{} {
260 | return &RequestInfo{}
261 | },
262 | },
263 | errorInfo: sync.Pool{
264 | New: func() interface{} {
265 | return &ErrorInfo{}
266 | },
267 | },
268 | httpCodeInfo: sync.Pool{
269 | New: func() interface{} {
270 | return &HttpCodeInfo{}
271 | },
272 | },
273 | },
274 | }
275 |
276 | go Stat.handleInfo()
277 | go time.AfterFunc(time.Duration(defaultCheckTimeMinutes)*time.Minute, Stat.gc)
278 | }
279 |
--------------------------------------------------------------------------------
/pkg/base/storage.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type (
8 | Storage struct {
9 | sync.Map
10 | }
11 | )
12 |
13 | func NewStorage() *Storage {
14 | return &Storage{}
15 | }
16 |
17 | func (s *Storage) All() map[string]interface{} {
18 | data := make(map[string]interface{})
19 | s.Range(func(key, value interface{}) bool {
20 |
21 | data[key.(string)] = value
22 | return true
23 | })
24 | return data
25 | }
26 |
27 | func (s *Storage) Exists(key interface{}) bool {
28 |
29 | _, ok := s.Load(key)
30 |
31 | return ok
32 | }
33 |
34 | func (s *Storage) GetUint64(key interface{}) (uint64, bool) {
35 | val, ok := s.Load(key)
36 | if !ok {
37 | return 0, false
38 | }
39 |
40 | ret, ok := val.(uint64)
41 | return ret, ok
42 | }
43 |
44 | func (s *Storage) Len() uint {
45 | var count uint
46 | s.Range(func(key, val interface{}) bool {
47 | count++
48 | return true
49 | })
50 |
51 | return count
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/crontab/crontab.go:
--------------------------------------------------------------------------------
1 | package crontab
2 |
3 | import (
4 | "container/heap"
5 | "errors"
6 | "jiacrontab/pkg/pqueue"
7 | "sync"
8 | "time"
9 | )
10 |
11 | // Task 任务
12 | type Task = pqueue.Item
13 |
14 | type Crontab struct {
15 | pq pqueue.PriorityQueue
16 | mux sync.RWMutex
17 | ready chan *Task
18 | }
19 |
20 | func New() *Crontab {
21 | return &Crontab{
22 | pq: pqueue.New(10000),
23 | ready: make(chan *Task, 10000),
24 | }
25 | }
26 |
27 | // AddJob 添加未经处理的job
28 | func (c *Crontab) AddJob(j *Job) error {
29 | nt, err := j.NextExecutionTime(time.Now())
30 | if err != nil {
31 | return errors.New("Invalid execution time")
32 | }
33 | c.mux.Lock()
34 | heap.Push(&c.pq, &Task{
35 | Priority: nt.UnixNano(),
36 | Value: j,
37 | })
38 | c.mux.Unlock()
39 | return nil
40 | }
41 |
42 | // AddJob 添加延时任务
43 | func (c *Crontab) AddTask(t *Task) {
44 | c.mux.Lock()
45 | heap.Push(&c.pq, t)
46 | c.mux.Unlock()
47 | }
48 |
49 | func (c *Crontab) Len() int {
50 | c.mux.RLock()
51 | len := len(c.pq)
52 | c.mux.RUnlock()
53 | return len
54 | }
55 |
56 | func (c *Crontab) GetAllTask() []*Task {
57 | c.mux.Lock()
58 | list := c.pq
59 | c.mux.Unlock()
60 | return list
61 | }
62 |
63 | func (c *Crontab) Ready() <-chan *Task {
64 | return c.ready
65 | }
66 |
67 | func (c *Crontab) QueueScanWorker() {
68 | refreshTicker := time.NewTicker(20 * time.Millisecond)
69 | for {
70 | select {
71 | case <-refreshTicker.C:
72 | if len(c.pq) == 0 {
73 | continue
74 | }
75 | start:
76 | c.mux.Lock()
77 | now := time.Now().UnixNano()
78 | job, _ := c.pq.PeekAndShift(now)
79 | c.mux.Unlock()
80 | if job == nil {
81 | continue
82 | }
83 | c.ready <- job
84 | goto start
85 |
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/crontab/crontab_test.go:
--------------------------------------------------------------------------------
1 | package crontab
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func Test_crontab_Ready(t *testing.T) {
11 | var timeLayout = "2006-01-02 15:04:05"
12 | c := New()
13 | now := time.Now().Add(6 * time.Second)
14 | c.AddTask(&Task{
15 | Value: "test1" + now.Format(timeLayout),
16 | Priority: now.UnixNano(),
17 | })
18 |
19 | now = time.Now().Add(1 * time.Second)
20 |
21 | c.AddTask(&Task{
22 | Value: "test2" + now.Format(timeLayout),
23 | Priority: now.UnixNano(),
24 | })
25 | now = time.Now().Add(3 * time.Second)
26 |
27 | c.AddTask(&Task{
28 | Value: "test3" + now.Format(timeLayout),
29 | Priority: now.UnixNano(),
30 | })
31 |
32 | now = time.Now().Add(4 * time.Second)
33 | c.AddTask(&Task{
34 | Value: "test4" + now.Format(timeLayout),
35 | Priority: now.UnixNano(),
36 | })
37 |
38 | now = time.Now().Add(3 * time.Second)
39 | c.AddTask(&Task{
40 | Value: "test5" + now.Format(timeLayout),
41 | Priority: now.UnixNano(),
42 | })
43 |
44 | bts, _ := json.MarshalIndent(c.GetAllTask(), "", "")
45 | fmt.Println(string(bts))
46 |
47 | go c.QueueScanWorker()
48 |
49 | go func() {
50 | for v := range c.Ready() {
51 | bts, _ := json.MarshalIndent(v, "", "")
52 | fmt.Println(string(bts))
53 | }
54 | }()
55 |
56 | time.Sleep(10 * time.Second)
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/crontab/job.go:
--------------------------------------------------------------------------------
1 | // package crontab 实现定时调度
2 | // 借鉴https://github.com/robfig/cron
3 | // 部分实现添加注释
4 | // 向https://github.com/robfig/cron项目致敬
5 | package crontab
6 |
7 | import (
8 | "errors"
9 | "fmt"
10 | "jiacrontab/pkg/util"
11 | "time"
12 | )
13 |
14 | const (
15 | starBit = 1 << 63
16 | )
17 |
18 | type bounds struct {
19 | min, max uint
20 | names map[string]uint
21 | }
22 |
23 | // The bounds for each field.
24 | var (
25 | seconds = bounds{0, 59, nil}
26 | minutes = bounds{0, 59, nil}
27 | hours = bounds{0, 23, nil}
28 | dom = bounds{1, 31, nil}
29 | months = bounds{1, 12, map[string]uint{
30 | "jan": 1,
31 | "feb": 2,
32 | "mar": 3,
33 | "apr": 4,
34 | "may": 5,
35 | "jun": 6,
36 | "jul": 7,
37 | "aug": 8,
38 | "sep": 9,
39 | "oct": 10,
40 | "nov": 11,
41 | "dec": 12,
42 | }}
43 | dow = bounds{0, 6, map[string]uint{
44 | "sun": 0,
45 | "mon": 1,
46 | "tue": 2,
47 | "wed": 3,
48 | "thu": 4,
49 | "fri": 5,
50 | "sat": 6,
51 | }}
52 | )
53 |
54 | type Job struct {
55 | Second string
56 | Minute string
57 | Hour string
58 | Day string
59 | Weekday string
60 | Month string
61 |
62 | ID uint
63 | now time.Time
64 | lastExecutionTime time.Time
65 | nextExecutionTime time.Time
66 |
67 | second, minute, hour, dom, month, dow uint64
68 |
69 | Value interface{}
70 | }
71 |
72 | func (j *Job) Format() string {
73 | return fmt.Sprintf("second: %s minute: %s hour: %s day: %s weekday: %s month: %s",
74 | j.Second, j.Minute, j.Hour, j.Day, j.Weekday, j.Month)
75 | }
76 | func (j *Job) GetNextExecTime() time.Time {
77 | return j.nextExecutionTime
78 | }
79 |
80 | func (j *Job) GetLastExecTime() time.Time {
81 | return j.lastExecutionTime
82 | }
83 |
84 | // parse 解析定时规则
85 | // 根据规则生成符和条件的日期
86 | // 例如:*/2 如果位于分位,则生成0,2,4,6....58
87 | // 生成的日期逐条的被映射到uint64数值中
88 | // min |= 1<<2
89 | func (j *Job) parse() error {
90 | var err error
91 | field := func(field string, r bounds) uint64 {
92 | if err != nil {
93 | return 0
94 | }
95 | var bits uint64
96 | bits, err = getField(field, r)
97 | return bits
98 | }
99 | j.second = field(j.Second, seconds)
100 | j.minute = field(j.Minute, minutes)
101 | j.hour = field(j.Hour, hours)
102 | j.dom = field(j.Day, dom)
103 | j.month = field(j.Month, months)
104 | j.dow = field(j.Weekday, dow)
105 |
106 | return err
107 |
108 | }
109 |
110 | // NextExecTime 获得下次执行时间
111 | func (j *Job) NextExecutionTime(t time.Time) (time.Time, error) {
112 | if err := j.parse(); err != nil {
113 | return time.Time{}, err
114 | }
115 |
116 | t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
117 | added := false
118 | defer func() {
119 | j.lastExecutionTime, j.nextExecutionTime = j.nextExecutionTime, t
120 | }()
121 |
122 | // 设置最大调度周期为5年
123 | yearLimit := t.Year() + 5
124 |
125 | WRAP:
126 | if t.Year() > yearLimit {
127 | return time.Time{}, errors.New("Over 5 years")
128 | }
129 |
130 | for 1< 0
206 | dowMatch bool = 1< 0
207 | )
208 |
209 | if j.dom&starBit > 0 || j.dow&starBit > 0 {
210 | return domMatch && dowMatch
211 | }
212 | return domMatch || dowMatch
213 | }
214 |
--------------------------------------------------------------------------------
/pkg/crontab/job_test.go:
--------------------------------------------------------------------------------
1 | package crontab
2 |
3 | import (
4 | "jiacrontab/pkg/test"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestJob_NextExecutionTime(t *testing.T) {
10 | timeLayout := "2006-01-02 15:04:05"
11 | j := &Job{
12 | Second: "48",
13 | Minute: "3",
14 | Hour: "12",
15 | Day: "25",
16 | Weekday: "*",
17 | Month: "1",
18 | }
19 |
20 | tt, err := j.NextExecutionTime(time.Now())
21 | test.Nil(t, err)
22 | test.Equal(t, "2020-01-25 12:03:48", tt.Format(timeLayout))
23 |
24 | tt, err = j.NextExecutionTime(tt)
25 | test.Nil(t, err)
26 | test.Equal(t, "2021-01-25 12:03:48", tt.Format(timeLayout))
27 |
28 | tt, err = j.NextExecutionTime(tt)
29 | test.Equal(t, "2022-01-25 12:03:48", tt.Format(timeLayout))
30 |
31 | j = &Job{
32 | Second: "58",
33 | Minute: "*/4",
34 | Hour: "12",
35 | Day: "4",
36 | Weekday: "*",
37 | Month: "3",
38 | }
39 | tt, err = j.NextExecutionTime(time.Now())
40 | test.Nil(t, err)
41 | test.Equal(t, "2020-03-04 12:00:58", tt.Format(timeLayout))
42 |
43 | tt, err = j.NextExecutionTime(tt)
44 | test.Nil(t, err)
45 | test.Equal(t, "2020-03-04 12:04:58", tt.Format(timeLayout))
46 |
47 | tt, err = j.NextExecutionTime(tt)
48 | test.Nil(t, err)
49 | test.Equal(t, "2020-03-04 12:08:58", tt.Format(timeLayout))
50 |
51 | j = &Job{
52 | Second: "0",
53 | Minute: "*",
54 | Hour: "*",
55 | Day: "*",
56 | Weekday: "*",
57 | Month: "*",
58 | }
59 |
60 | tt, err = j.NextExecutionTime(time.Now())
61 | test.Nil(t, err)
62 | t.Log(tt, j.GetLastExecTime())
63 | for i := 0; i < 1000; i++ {
64 | tt, err = j.NextExecutionTime(tt)
65 | test.Nil(t, err)
66 | t.Log(tt, j.GetLastExecTime())
67 | }
68 |
69 | t.Log("end")
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/crontab/parse.go:
--------------------------------------------------------------------------------
1 | package crontab
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | func getRange(expr string, r bounds) (uint64, error) {
11 | var (
12 | start, end, step uint
13 | rangeAndStep = strings.Split(expr, "/")
14 | lowAndHigh = strings.Split(rangeAndStep[0], "-")
15 | singleDigit = len(lowAndHigh) == 1
16 | err error
17 | )
18 |
19 | var extra uint64
20 | if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
21 | start = r.min
22 | end = r.max
23 | extra = starBit
24 | } else {
25 | if lowAndHigh[0] == "L" {
26 | return 0, nil
27 | }
28 | start, err = parseIntOrName(lowAndHigh[0], r.names)
29 | if err != nil {
30 | return 0, err
31 | }
32 |
33 | switch len(lowAndHigh) {
34 | case 1:
35 | end = start
36 | case 2:
37 | end, err = parseIntOrName(lowAndHigh[1], r.names)
38 | if err != nil {
39 | return 0, err
40 | }
41 | default:
42 | return 0, fmt.Errorf("Too many hyphens: %s", expr)
43 | }
44 | }
45 |
46 | switch len(rangeAndStep) {
47 | case 1:
48 | step = 1
49 | case 2:
50 | step, err = mustParseInt(rangeAndStep[1])
51 | if err != nil {
52 | return 0, err
53 | }
54 |
55 | // Special handling: "N/step" means "N-max/step".
56 | if singleDigit {
57 | end = r.max
58 | }
59 | default:
60 | return 0, fmt.Errorf("Too many slashes: %s", expr)
61 | }
62 |
63 | if start < r.min {
64 | return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
65 | }
66 | if end > r.max {
67 | return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
68 | }
69 | if start > end {
70 | return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
71 | }
72 | if step == 0 {
73 | return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
74 | }
75 |
76 | return getBits(start, end, step) | extra, nil
77 | }
78 |
79 | func parseIntOrName(expr string, names map[string]uint) (uint, error) {
80 | if names != nil {
81 | if namedInt, ok := names[strings.ToLower(expr)]; ok {
82 | return namedInt, nil
83 | }
84 | }
85 | return mustParseInt(expr)
86 | }
87 |
88 | // mustParseInt parses the given expression as an int or returns an error.
89 | func mustParseInt(expr string) (uint, error) {
90 | num, err := strconv.Atoi(expr)
91 | if err != nil {
92 | return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
93 | }
94 | if num < 0 {
95 | return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
96 | }
97 |
98 | return uint(num), nil
99 | }
100 |
101 | // getBits sets all bits in the range [min, max], modulo the given step size.
102 | func getBits(min, max, step uint) uint64 {
103 | var bits uint64
104 |
105 | // If step is 1, use shifts.
106 | if step == 1 {
107 | return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
108 | }
109 |
110 | // Else, use a simple loop.
111 | for i := min; i <= max; i += step {
112 | bits |= 1 << i
113 | }
114 | return bits
115 | }
116 |
117 | func getField(field string, r bounds) (uint64, error) {
118 | var bits uint64
119 | ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
120 | for _, expr := range ranges {
121 | bit, err := getRange(expr, r)
122 | if err != nil {
123 | return bits, err
124 | }
125 | bits |= bit
126 | }
127 | return bits, nil
128 | }
129 |
--------------------------------------------------------------------------------
/pkg/file/file.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "math"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 |
13 | "github.com/iwannay/log"
14 | )
15 |
16 | func Exist(path string) bool {
17 | _, err := os.Stat(path)
18 | return err == nil || os.IsExist(err)
19 | }
20 |
21 | func GetCurrentDirectory() string {
22 | dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
23 | if err != nil {
24 | log.Error(err)
25 | return ""
26 | }
27 | return filepath.Clean(strings.Replace(dir, "\\", "/", -1))
28 |
29 | }
30 |
31 | func IsTextFile(data []byte) bool {
32 | if len(data) == 0 {
33 | return true
34 | }
35 | return strings.Contains(http.DetectContentType(data), "text/")
36 | }
37 | func IsImageFile(data []byte) bool {
38 | return strings.Contains(http.DetectContentType(data), "image/")
39 | }
40 | func IsPDFFile(data []byte) bool {
41 | return strings.Contains(http.DetectContentType(data), "application/pdf")
42 | }
43 | func IsVideoFile(data []byte) bool {
44 | return strings.Contains(http.DetectContentType(data), "video/")
45 | }
46 |
47 | const (
48 | Byte = 1
49 | KByte = Byte * 1024
50 | MByte = KByte * 1024
51 | GByte = MByte * 1024
52 | TByte = GByte * 1024
53 | PByte = TByte * 1024
54 | EByte = PByte * 1024
55 | )
56 |
57 | var bytesSizeTable = map[string]uint64{
58 | "b": Byte,
59 | "kb": KByte,
60 | "mb": MByte,
61 | "gb": GByte,
62 | "tb": TByte,
63 | "pb": PByte,
64 | "eb": EByte,
65 | }
66 |
67 | func logn(n, b float64) float64 {
68 | return math.Log(n) / math.Log(b)
69 | }
70 | func humanateBytes(s uint64, base float64, sizes []string) string {
71 | if s < 10 {
72 | return fmt.Sprintf("%d B", s)
73 | }
74 | e := math.Floor(logn(float64(s), base))
75 | suffix := sizes[int(e)]
76 | val := float64(s) / math.Pow(base, math.Floor(e))
77 | f := "%.0f"
78 | if val < 10 {
79 | f = "%.1f"
80 | }
81 | return fmt.Sprintf(f+" %s", val, suffix)
82 | }
83 | func FileSize(s int64) string {
84 | sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
85 | return humanateBytes(uint64(s), 1024, sizes)
86 | }
87 |
88 | func CreateFile(path string) (*os.File, error) {
89 | err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
90 | if err != nil {
91 | return nil, err
92 | }
93 | return os.Create(path)
94 | }
95 |
96 | func DirSize(dir string) int64 {
97 | var total int64
98 | filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
99 | if info == nil {
100 | return nil
101 | }
102 | if !info.IsDir() {
103 | total += info.Size()
104 | }
105 | return nil
106 | })
107 | return total
108 | }
109 |
110 | func Remove(dir string, t time.Time) (total int64, size int64, err error) {
111 | err = filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
112 | if info == nil {
113 | return errors.New(fpath + " not exists")
114 | }
115 | if !info.IsDir() {
116 | if info.ModTime().Before(t) {
117 | total++
118 | size += info.Size()
119 | err = os.Remove(fpath)
120 | }
121 | } else {
122 | // 删除空目录
123 | err = os.Remove(fpath)
124 | if err == nil {
125 | total++
126 | log.Println("delete ", fpath)
127 | }
128 | err = nil
129 | }
130 |
131 | return err
132 | })
133 | return
134 | }
135 |
--------------------------------------------------------------------------------
/pkg/finder/finder.go:
--------------------------------------------------------------------------------
1 | package finder
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "errors"
7 | "jiacrontab/pkg/file"
8 | "log"
9 | "os"
10 | "path/filepath"
11 | "regexp"
12 | "sort"
13 | "time"
14 | )
15 |
16 | type matchDataChunk struct {
17 | modifyTime time.Time
18 | matchData []byte
19 | }
20 |
21 | type DataQueue []matchDataChunk
22 |
23 | func (d DataQueue) Swap(i, j int) {
24 | d[i], d[j] = d[j], d[i]
25 | }
26 | func (d DataQueue) Less(i, j int) bool {
27 | return d[i].modifyTime.Unix() < d[j].modifyTime.Unix()
28 | }
29 | func (d DataQueue) Len() int {
30 | return len(d)
31 | }
32 |
33 | type Finder struct {
34 | matchDataQueue DataQueue
35 | curr int32
36 | regexp *regexp.Regexp
37 | pagesize int
38 | errors []error
39 | patternAll bool
40 | filter func(os.FileInfo) bool
41 | isTail bool
42 | offset int64
43 | fileSize int64
44 | }
45 |
46 | func NewFinder(filter func(os.FileInfo) bool) *Finder {
47 | return &Finder{
48 | filter: filter,
49 | }
50 | }
51 |
52 | func (fd *Finder) SetTail(flag bool) {
53 | fd.isTail = flag
54 | }
55 |
56 | func (fd *Finder) Offset() int64 {
57 | return fd.offset
58 | }
59 |
60 | func (fd *Finder) HumanateFileSize() string {
61 | return file.FileSize(fd.fileSize)
62 | }
63 |
64 | func (fd *Finder) FileSize() int64 {
65 | return fd.fileSize
66 | }
67 |
68 | func (fd *Finder) find(fpath string, modifyTime time.Time) error {
69 |
70 | var matchData []byte
71 | var reader *bufio.Reader
72 |
73 | f, err := os.Open(fpath)
74 | if err != nil {
75 | return err
76 | }
77 | defer f.Close()
78 |
79 | info, err := f.Stat()
80 | if err != nil {
81 | return err
82 | }
83 |
84 | fd.fileSize = info.Size()
85 |
86 | if fd.fileSize < fd.offset {
87 | return errors.New("out of file")
88 | }
89 |
90 | if fd.isTail {
91 | if fd.offset < 0 {
92 | fd.offset = fd.fileSize
93 | }
94 | f.Seek(fd.offset, 0)
95 | reader = bufio.NewReader(NewTailReader(f, fd.offset))
96 | } else {
97 | f.Seek(fd.offset, 0)
98 | reader = bufio.NewReader(f)
99 | }
100 |
101 | for {
102 |
103 | bts, _ := reader.ReadBytes('\n')
104 |
105 | if len(bts) == 0 {
106 | break
107 | }
108 |
109 | if fd.isTail {
110 | fd.offset -= int64(len(bts))
111 | } else {
112 | fd.offset += int64(len(bts))
113 | }
114 |
115 | if fd.isTail {
116 | if fd.offset == 0 {
117 | bts = append(bts, '\n')
118 | }
119 | invert(bts)
120 | }
121 |
122 | if fd.patternAll || fd.regexp.Match(bts) {
123 | matchData = append(matchData, bts...)
124 | fd.curr++
125 | }
126 |
127 | if fd.curr >= int32(fd.pagesize) {
128 | break
129 | }
130 |
131 | if fd.offset <= 0 {
132 | break
133 | }
134 |
135 | }
136 |
137 | if len(matchData) > 0 {
138 | fd.matchDataQueue = append(fd.matchDataQueue, matchDataChunk{
139 | modifyTime: modifyTime,
140 | matchData: bytes.TrimLeft(bytes.TrimRight(matchData, "\n"), "\n"),
141 | })
142 | }
143 | return nil
144 | }
145 |
146 | func (fd *Finder) walkFunc(fpath string, info os.FileInfo, err error) error {
147 | if !info.IsDir() {
148 | if fd.filter != nil && fd.filter(info) {
149 | err := fd.find(fpath, info.ModTime())
150 | if err != nil {
151 | fd.errors = append(fd.errors, err)
152 | }
153 | }
154 |
155 | }
156 |
157 | return nil
158 | }
159 |
160 | func (fd *Finder) Search(root string, expr string, data *[]byte, offset int64, pagesize int) error {
161 | var err error
162 | fd.pagesize = pagesize
163 | fd.offset = offset
164 |
165 | if expr == "" {
166 | fd.patternAll = true
167 | }
168 |
169 | if !file.Exist(root) {
170 | return errors.New(root + " not exist")
171 | }
172 |
173 | fd.regexp, err = regexp.Compile(expr)
174 | if err != nil {
175 | return err
176 | }
177 | filepath.Walk(root, fd.walkFunc)
178 |
179 | sort.Stable(fd.matchDataQueue)
180 | for _, v := range fd.matchDataQueue {
181 | *data = append(*data, v.matchData...)
182 | }
183 | return nil
184 | }
185 |
186 | func (fd *Finder) GetErrors() []error {
187 | return fd.errors
188 | }
189 |
190 | func SearchAndDeleteFileOnDisk(dir string, d time.Duration, size int64) {
191 | t := time.NewTicker(1 * time.Minute)
192 | for {
193 | select {
194 | case <-t.C:
195 | filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
196 | if info == nil {
197 | return errors.New(fpath + "not exists")
198 | }
199 | if !info.IsDir() {
200 | if time.Now().Sub(info.ModTime()) > d {
201 | err = os.Remove(fpath)
202 |
203 | }
204 |
205 | if info.Size() > size && size != 0 {
206 | err = os.Remove(fpath)
207 |
208 | }
209 | } else {
210 | // 删除空目录
211 | err := os.Remove(fpath)
212 | if err == nil {
213 | log.Println("delete ", fpath)
214 | }
215 | }
216 | return err
217 | })
218 | }
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/pkg/finder/reader.go:
--------------------------------------------------------------------------------
1 | package finder
2 |
3 | import (
4 | "io"
5 | "os"
6 | )
7 |
8 | type TailReader struct {
9 | f *os.File
10 | curr int64
11 | isEOF bool
12 | }
13 |
14 | func (t *TailReader) Read(b []byte) (n int, err error) {
15 | if t.isEOF {
16 | return 0, io.EOF
17 | }
18 |
19 | off := t.curr - int64(len(b))
20 | if off < 0 {
21 | off = 0
22 | n, err = t.f.ReadAt(b[0:t.curr], off)
23 | } else {
24 | t.curr = off
25 | n, err = t.f.ReadAt(b, off)
26 | }
27 |
28 | if err != nil && err != io.EOF {
29 | return n, err
30 | }
31 |
32 | invert(b[0:n])
33 |
34 | if off == 0 {
35 | t.isEOF = true
36 | }
37 |
38 | return
39 | }
40 |
41 | func NewTailReader(f *os.File, offset int64) io.Reader {
42 | return &TailReader{
43 | f: f,
44 | curr: offset,
45 | }
46 |
47 | }
48 |
49 | func invert(b []byte) {
50 | for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
51 | b[i], b[j] = b[j], b[i]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/kproc/proc.go:
--------------------------------------------------------------------------------
1 | package kproc
2 |
3 | import (
4 | "context"
5 | "jiacrontab/pkg/file"
6 | "os/exec"
7 | )
8 |
9 | type KCmd struct {
10 | ctx context.Context
11 | *exec.Cmd
12 | isKillChildProcess bool
13 | done chan struct{}
14 | }
15 |
16 | // SetEnv 设置环境变量
17 | func (k *KCmd) SetEnv(env []string) {
18 | if len(env) == 0 {
19 | return
20 | }
21 | k.Cmd.Env = env
22 | }
23 |
24 | // SetDir 设置工作目录
25 | func (k *KCmd) SetDir(dir string) {
26 | if dir == "" {
27 | return
28 | }
29 | if file.Exist(dir) == false {
30 | return
31 | }
32 | k.Cmd.Dir = dir
33 | }
34 |
35 | // SetExitKillChildProcess 设置主进程退出时是否kill子进程,默认kill
36 | func (k *KCmd) SetExitKillChildProcess(ok bool) {
37 | k.isKillChildProcess = ok
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/kproc/proc_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package kproc
4 |
5 | import (
6 | "context"
7 | "os"
8 | "os/exec"
9 | "os/user"
10 | "strconv"
11 | "syscall"
12 |
13 | "github.com/iwannay/log"
14 | )
15 |
16 | func CommandContext(ctx context.Context, name string, arg ...string) *KCmd {
17 | cmd := exec.CommandContext(ctx, name, arg...)
18 | cmd.SysProcAttr = &syscall.SysProcAttr{}
19 | cmd.SysProcAttr.Setsid = true
20 | return &KCmd{
21 | ctx: ctx,
22 | Cmd: cmd,
23 | isKillChildProcess: true,
24 | done: make(chan struct{}),
25 | }
26 | }
27 |
28 | // SetUser 设置执行用户要保证root权限
29 | func (k *KCmd) SetUser(username string) {
30 | if username == "" {
31 | return
32 | }
33 | u, err := user.Lookup(username)
34 | if err != nil {
35 | log.Error("setUser error:", err)
36 | return
37 | }
38 |
39 | log.Infof("KCmd set uid=%s,gid=%s", u.Uid, u.Gid)
40 | k.SysProcAttr = &syscall.SysProcAttr{}
41 | uid, _ := strconv.Atoi(u.Uid)
42 | gid, _ := strconv.Atoi(u.Gid)
43 | k.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
44 |
45 | }
46 |
47 | func (k *KCmd) KillAll() {
48 |
49 | select {
50 | case k.done <- struct{}{}:
51 | default:
52 | }
53 |
54 | if k.Process == nil {
55 | return
56 | }
57 |
58 | if k.isKillChildProcess == false {
59 | return
60 | }
61 |
62 | group, err := os.FindProcess(-k.Process.Pid)
63 | if err == nil {
64 | group.Signal(syscall.SIGKILL)
65 | }
66 | }
67 |
68 | func (k *KCmd) Wait() error {
69 | defer k.KillAll()
70 | go func() {
71 | select {
72 | case <-k.ctx.Done():
73 | k.KillAll()
74 | case <-k.done:
75 | }
76 | }()
77 | return k.Cmd.Wait()
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/kproc/proc_windows.go:
--------------------------------------------------------------------------------
1 | package kproc
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os/exec"
7 | )
8 |
9 | func CommandContext(ctx context.Context, name string, arg ...string) *KCmd {
10 | cmd := exec.CommandContext(ctx, name, arg...)
11 | return &KCmd{
12 | Cmd: cmd,
13 | ctx: ctx,
14 | isKillChildProcess: true,
15 | done: make(chan struct{}),
16 | }
17 | }
18 |
19 | func (k *KCmd) SetUser(username string) {
20 | // TODO:windows切换用户
21 | }
22 |
23 | func (k *KCmd) KillAll() {
24 | select {
25 | case k.done <- struct{}{}:
26 | default:
27 | }
28 | if k.Process == nil {
29 | return
30 | }
31 |
32 | if k.isKillChildProcess == false {
33 | return
34 | }
35 |
36 | c := exec.Command("taskkill", "/t", "/f", "/pid", fmt.Sprint(k.Process.Pid))
37 | c.Stdout = k.Cmd.Stdout
38 | c.Stderr = k.Cmd.Stderr
39 | }
40 |
41 | func (k *KCmd) Wait() error {
42 | defer k.KillAll()
43 | go func() {
44 | select {
45 | case <-k.ctx.Done():
46 | k.KillAll()
47 | case <-k.done:
48 | }
49 | }()
50 | return k.Cmd.Wait()
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/mailer/login.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "fmt"
5 | "net/smtp"
6 | )
7 |
8 | type loginAuth struct {
9 | username, password string
10 | }
11 |
12 | // SMTP AUTH LOGIN Auth Handler
13 | func LoginAuth(username, password string) smtp.Auth {
14 | return &loginAuth{username, password}
15 | }
16 | func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
17 | return "LOGIN", []byte{}, nil
18 | }
19 | func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
20 | if more {
21 | switch string(fromServer) {
22 | case "Username:":
23 | return []byte(a.username), nil
24 | case "Password:":
25 | return []byte(a.password), nil
26 | default:
27 | return nil, fmt.Errorf("unknwon fromServer: %s", string(fromServer))
28 | }
29 | }
30 | return nil, nil
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/mailer/mail.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "crypto/tls"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 | "net/smtp"
10 | "os"
11 | "strings"
12 | "time"
13 |
14 | "github.com/iwannay/log"
15 |
16 | "gopkg.in/gomail.v2"
17 | )
18 |
19 | var (
20 | MailConfig *Mailer
21 | mailQueue chan *Message
22 | )
23 |
24 | type Mailer struct {
25 | QueueLength int
26 | SubjectPrefix string
27 | Host string
28 | From string
29 | FromEmail string
30 | User, Passwd string
31 | DisableHelo bool
32 | HeloHostname string
33 | SkipVerify bool
34 | UseCertificate bool
35 | CertFile, KeyFile string
36 | UsePlainText bool
37 | HookMode bool
38 | }
39 |
40 | type Message struct {
41 | *gomail.Message
42 | Info string
43 | confirmChan chan struct{}
44 | }
45 |
46 | func NewMessage(to []string, subject, htmlBody string) *Message {
47 | return NewMessageFrom(to, MailConfig.From, subject, htmlBody)
48 | }
49 |
50 | func NewMessageFrom(to []string, from, subject, htmlBody string) *Message {
51 | log.Printf("QueueLength (%d) NewMessage (htmlBody) \n%s\n", len(mailQueue), htmlBody)
52 | msg := gomail.NewMessage()
53 | msg.SetHeader("From", from)
54 | msg.SetHeader("To", to...)
55 | msg.SetHeader("Subject", subject)
56 | msg.SetDateHeader("Date", time.Now())
57 | contentType := "text/html"
58 |
59 | msg.SetBody(contentType, htmlBody)
60 | return &Message{
61 | Message: msg,
62 | confirmChan: make(chan struct{}),
63 | }
64 | }
65 |
66 | type Sender struct {
67 | }
68 |
69 | func (s *Sender) Send(from string, to []string, msg io.WriterTo) error {
70 | host, port, err := net.SplitHostPort(MailConfig.Host)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | tlsConfig := &tls.Config{
76 | InsecureSkipVerify: MailConfig.SkipVerify,
77 | ServerName: host,
78 | }
79 | if MailConfig.UseCertificate {
80 | cert, err := tls.LoadX509KeyPair(MailConfig.CertFile, MailConfig.KeyFile)
81 | if err != nil {
82 | return err
83 | }
84 | tlsConfig.Certificates = []tls.Certificate{cert}
85 | }
86 |
87 | conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 3*time.Second)
88 | if err != nil {
89 | return err
90 | }
91 | defer conn.Close()
92 | isSecureConn := false
93 | if strings.HasSuffix(port, "465") {
94 | conn = tls.Client(conn, tlsConfig)
95 | isSecureConn = true
96 | }
97 | client, err := smtp.NewClient(conn, host)
98 | if err != nil {
99 | return fmt.Errorf("NewClient: %v", err)
100 | }
101 |
102 | if MailConfig.DisableHelo {
103 | hostname := MailConfig.HeloHostname
104 | if len(hostname) == 0 {
105 | hostname, err = os.Hostname()
106 | if err != nil {
107 | return err
108 | }
109 | }
110 |
111 | if err = client.Hello(hostname); err != nil {
112 | return fmt.Errorf("Hello:%v", err)
113 | }
114 | }
115 |
116 | hasStartTLS, _ := client.Extension("STARTTLS")
117 | if !isSecureConn && hasStartTLS {
118 | if err = client.StartTLS(tlsConfig); err != nil {
119 | return fmt.Errorf("StartTLS:%v", err)
120 | }
121 | }
122 |
123 | canAuth, options := client.Extension("AUTH")
124 |
125 | if canAuth && len(MailConfig.User) > 0 {
126 | var auth smtp.Auth
127 | if strings.Contains(options, "CRAM-MD5") {
128 | auth = smtp.CRAMMD5Auth(MailConfig.User, MailConfig.Passwd)
129 | } else if strings.Contains(options, "PLAIN") {
130 | auth = smtp.PlainAuth("", MailConfig.User, MailConfig.Passwd, host)
131 | } else if strings.Contains(options, "LOGIN") {
132 | // Patch for AUTH LOGIN
133 | auth = LoginAuth(MailConfig.User, MailConfig.Passwd)
134 | }
135 | if auth != nil {
136 | if err = client.Auth(auth); err != nil {
137 | return fmt.Errorf("Auth: %v", err)
138 | }
139 | }
140 | }
141 |
142 | if err = client.Mail(from); err != nil {
143 | return fmt.Errorf("Mail: %v", err)
144 | }
145 | for _, rec := range to {
146 | if err = client.Rcpt(rec); err != nil {
147 | return fmt.Errorf("Rcpt: %v", err)
148 | }
149 | }
150 | w, err := client.Data()
151 | if err != nil {
152 | return fmt.Errorf("Data: %v", err)
153 | } else if _, err = msg.WriteTo(w); err != nil {
154 | return fmt.Errorf("WriteTo: %v", err)
155 | } else if err = w.Close(); err != nil {
156 | return fmt.Errorf("Close: %v", err)
157 | }
158 | return client.Quit()
159 | }
160 |
161 | func processMailQueue() {
162 | sender := &Sender{}
163 | for {
164 | select {
165 | case msg := <-mailQueue:
166 | if err := gomail.Send(sender, msg.Message); err != nil {
167 | log.Errorf("Fail to send emails %s: %s - %v\n", msg.GetHeader("To"), msg.Info, err)
168 | } else {
169 | log.Infof("E-mails sent %s: %s\n", msg.GetHeader("To"), msg.Info)
170 | }
171 | msg.confirmChan <- struct{}{}
172 | }
173 | }
174 | }
175 |
176 | func InitMailer(m *Mailer) {
177 | MailConfig = m
178 | if MailConfig == nil || mailQueue != nil {
179 | return
180 | }
181 |
182 | mailQueue = make(chan *Message, MailConfig.QueueLength)
183 | go processMailQueue()
184 | }
185 |
186 | func Send(msg *Message) {
187 | mailQueue <- msg
188 | if MailConfig.HookMode {
189 | <-msg.confirmChan
190 | return
191 | }
192 | go func() {
193 | <-msg.confirmChan
194 | }()
195 | }
196 |
197 | func SendMail(to []string, subject, content string) error {
198 | if MailConfig == nil {
199 | return errors.New("update mail config must restart service")
200 | }
201 | msg := NewMessage(to, subject, content)
202 | Send(msg)
203 | return nil
204 | }
205 |
--------------------------------------------------------------------------------
/pkg/pprof/pprof.go:
--------------------------------------------------------------------------------
1 | package pprof
2 |
3 | import (
4 | "jiacrontab/pkg/file"
5 | "path/filepath"
6 | "runtime"
7 | "runtime/pprof"
8 | "time"
9 |
10 | "github.com/iwannay/log"
11 | )
12 |
13 | func ListenPprof() {
14 | go listenSignal()
15 | }
16 |
17 | func cpuprofile() {
18 | path := filepath.Join("pprof", "cpuprofile")
19 | log.Debugf("profile save in %s", path)
20 |
21 | f, err := file.CreateFile(path)
22 | if err != nil {
23 | log.Error("could not create CPU profile: ", err)
24 | return
25 | }
26 |
27 | defer f.Close()
28 |
29 | if err := pprof.StartCPUProfile(f); err != nil {
30 | log.Error("could not start CPU profile: ", err)
31 | } else {
32 | time.Sleep(time.Minute)
33 | }
34 | defer pprof.StopCPUProfile()
35 | }
36 |
37 | func memprofile() {
38 | path := filepath.Join("pprof", "memprofile")
39 | log.Debugf("profile save in %s", path)
40 | f, err := file.CreateFile(path)
41 | if err != nil {
42 | log.Error("could not create memory profile: ", err)
43 | return
44 | }
45 |
46 | defer f.Close()
47 |
48 | runtime.GC() // get up-to-date statistics
49 |
50 | if err := pprof.WriteHeapProfile(f); err != nil {
51 | log.Error("could not write memory profile: ", err)
52 | }
53 | }
54 |
55 | func profile() {
56 | names := []string{
57 | "goroutine",
58 | "heap",
59 | "allocs",
60 | "threadcreate",
61 | "block",
62 | "mutex",
63 | }
64 | for _, name := range names {
65 | path := filepath.Join("pprof", name)
66 | log.Debugf("profile save in %s", path)
67 | f, err := file.CreateFile(path)
68 | if err != nil {
69 | log.Error(err)
70 | continue
71 | }
72 | pprof.Lookup(name).WriteTo(f, 0)
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/pprof/pprof_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package pprof
4 |
5 | import (
6 | "os"
7 | "os/signal"
8 | "syscall"
9 | )
10 |
11 | func listenSignal() {
12 | signChan := make(chan os.Signal, 1)
13 | signal.Notify(signChan, syscall.SIGUSR1)
14 | for {
15 | <-signChan
16 | profile()
17 | memprofile()
18 | cpuprofile()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/pprof/pprof_windows.go:
--------------------------------------------------------------------------------
1 | package pprof
2 |
3 | func listenSignal() {
4 | }
5 |
--------------------------------------------------------------------------------
/pkg/pqueue/pqueue.go:
--------------------------------------------------------------------------------
1 | // Package pqueue jiacrontab中使用的优先队列
2 | // 参考nsq的实现
3 | // 做了注释和少量调整
4 | package pqueue
5 |
6 | import (
7 | "container/heap"
8 | )
9 |
10 | type Item struct {
11 | Value interface{}
12 | Priority int64
13 | Index int
14 | }
15 |
16 | // PriorityQueue 最小堆实现的优先队列
17 | type PriorityQueue []*Item
18 |
19 | // New 创建
20 | func New(capacity int) PriorityQueue {
21 | return make(PriorityQueue, 0, capacity)
22 | }
23 |
24 | // Len 队列长队
25 | func (pq PriorityQueue) Len() int {
26 | return len(pq)
27 | }
28 |
29 | // Less 比较相邻两个原素优先级
30 | func (pq PriorityQueue) Less(i, j int) bool {
31 | return pq[i].Priority < pq[j].Priority
32 | }
33 |
34 | // Swap 交换相邻原素
35 | func (pq PriorityQueue) Swap(i, j int) {
36 | pq[i], pq[j] = pq[j], pq[i]
37 | pq[i].Index = i
38 | pq[j].Index = j
39 | }
40 |
41 | // Push 添加新的item
42 | func (pq *PriorityQueue) Push(x interface{}) {
43 | n := len(*pq)
44 | c := cap(*pq)
45 | if n+1 > c {
46 | npq := make(PriorityQueue, n, c*2)
47 | copy(npq, *pq)
48 | *pq = npq
49 | }
50 | *pq = (*pq)[0 : n+1]
51 | item := x.(*Item)
52 | item.Index = n
53 | (*pq)[n] = item
54 | }
55 |
56 | func (pq *PriorityQueue) update(item *Item, value string, priority int64) {
57 | item.Value = value
58 | item.Priority = priority
59 | heap.Fix(pq, item.Index)
60 | }
61 |
62 | // Pop 弹出队列末端原素
63 | func (pq *PriorityQueue) Pop() interface{} {
64 | n := len(*pq)
65 | c := cap(*pq)
66 | if n < (c/2) && c > 25 {
67 | npq := make(PriorityQueue, n, c/2)
68 | copy(npq, *pq)
69 | *pq = npq
70 | }
71 | item := (*pq)[n-1]
72 | item.Index = -1
73 | *pq = (*pq)[0 : n-1]
74 | return item
75 | }
76 |
77 | // PeekAndShift 根据比较max并弹出原素
78 | func (pq *PriorityQueue) PeekAndShift(max int64) (*Item, int64) {
79 | if pq.Len() == 0 {
80 | return nil, 0
81 | }
82 |
83 | item := (*pq)[0]
84 | if item.Priority > max {
85 | return nil, item.Priority - max
86 | }
87 | heap.Remove(pq, 0)
88 | return item, 0
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/pqueue/pqueue_test.go:
--------------------------------------------------------------------------------
1 | package pqueue
2 |
3 | import (
4 | "container/heap"
5 | "math/rand"
6 | "path/filepath"
7 | "reflect"
8 | "runtime"
9 | "sort"
10 | "testing"
11 | )
12 |
13 | func equal(t *testing.T, act, exp interface{}) {
14 | if !reflect.DeepEqual(exp, act) {
15 | _, file, line, _ := runtime.Caller(1)
16 | t.Logf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n",
17 | filepath.Base(file), line, exp, act)
18 | t.FailNow()
19 | }
20 | }
21 |
22 | func TestPriorityQueue(t *testing.T) {
23 | c := 100
24 | pq := New(c)
25 |
26 | for i := 0; i < c+1; i++ {
27 | heap.Push(&pq, &Item{Value: i, Priority: int64(i)})
28 | }
29 | equal(t, pq.Len(), c+1)
30 | equal(t, cap(pq), c*2)
31 |
32 | for i := 0; i < c+1; i++ {
33 | item := heap.Pop(&pq)
34 | equal(t, item.(*Item).Value.(int), i)
35 | }
36 | equal(t, cap(pq), c/4)
37 | }
38 |
39 | func TestUnsortedInsert(t *testing.T) {
40 | c := 100
41 | pq := New(c)
42 | ints := make([]int, 0, c)
43 |
44 | for i := 0; i < c; i++ {
45 | v := rand.Int()
46 | ints = append(ints, v)
47 | heap.Push(&pq, &Item{Value: i, Priority: int64(v)})
48 | }
49 | equal(t, pq.Len(), c)
50 | equal(t, cap(pq), c)
51 |
52 | sort.Ints(ints)
53 |
54 | for i := 0; i < c; i++ {
55 | item, _ := pq.PeekAndShift(int64(ints[len(ints)-1]))
56 | equal(t, item.Priority, int64(ints[i]))
57 | }
58 | }
59 |
60 | func TestRemove(t *testing.T) {
61 | c := 100
62 | pq := New(c)
63 |
64 | for i := 0; i < c; i++ {
65 | v := rand.Int()
66 | heap.Push(&pq, &Item{Value: "test", Priority: int64(v)})
67 | }
68 |
69 | for i := 0; i < 10; i++ {
70 | heap.Remove(&pq, rand.Intn((c-1)-i))
71 | }
72 |
73 | lastPriority := heap.Pop(&pq).(*Item).Priority
74 | for i := 0; i < (c - 10 - 1); i++ {
75 | item := heap.Pop(&pq)
76 | equal(t, lastPriority < item.(*Item).Priority, true)
77 | lastPriority = item.(*Item).Priority
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/proto/apicode.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | const (
4 | Code_Success = 0
5 | Code_FailedAuth = 5001
6 | Code_Error = 5002
7 | Code_NotFound = 5004
8 | Code_NotAllowed = 5005
9 | Code_JWTError = 5006
10 | Code_RPCError = 5007
11 | Code_ParamsError = 5008
12 | Code_DBError = 5009
13 | )
14 |
15 | const (
16 | Msg_NotAllowed = "permission not allowed"
17 | Msg_JWTError = "parse jwt token failed"
18 | )
19 |
--------------------------------------------------------------------------------
/pkg/proto/args.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import (
4 | "jiacrontab/models"
5 | )
6 |
7 | type SearchLog struct {
8 | JobID uint
9 | GroupID uint
10 | UserID uint
11 | Root bool
12 | IsTail bool
13 | Offset int64
14 | Pagesize int
15 | Date string
16 | Pattern string
17 | }
18 |
19 | type CleanNodeLog struct {
20 | Unit string
21 | Offset int
22 | }
23 |
24 | type CleanNodeLogRet struct {
25 | Total int64 `json:"total"`
26 | Size string `json:"size"`
27 | }
28 |
29 | type SearchLogResult struct {
30 | Content []byte
31 | Offset int64
32 | FileSize int64
33 | }
34 |
35 | type SendMail struct {
36 | MailTo []string
37 | Subject string
38 | Content string
39 | }
40 |
41 | type ApiPost struct {
42 | Urls []string
43 | Data string
44 | }
45 |
46 | type ExecCrontabJobReply struct {
47 | Job models.CrontabJob
48 | Content []byte
49 | }
50 |
51 | type ActionJobsArgs struct {
52 | UserID uint
53 | Root bool
54 | GroupID uint
55 | JobIDs []uint
56 | }
57 |
58 | type GetJobArgs struct {
59 | UserID uint
60 | GroupID uint
61 | Root bool
62 | JobID uint
63 | }
64 | type EmptyArgs struct{}
65 |
66 | type EmptyReply struct{}
67 |
--------------------------------------------------------------------------------
/pkg/proto/const.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | const (
4 | DefaultTimeLayout = "2006-01-02 15:04:05"
5 |
6 | TimeoutTrigger_CallApi = "CallApi"
7 | TimeoutTrigger_SendEmail = "SendEmail"
8 | TimeoutTrigger_Kill = "Kill"
9 | TimeoutTrigger_DingdingWebhook = "DingdingWebhook"
10 | )
11 |
--------------------------------------------------------------------------------
/pkg/proto/crontab.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import (
4 | "jiacrontab/models"
5 | "time"
6 | )
7 |
8 | type DepJobs []DepJob
9 | type DepJob struct {
10 | Name string
11 | Dest string
12 | From string
13 | ProcessID int // 当前主任务进程id
14 | ID string // 依赖任务id
15 | JobID uint // 主任务id
16 | JobUniqueID string // 主任务唯一标志
17 | Commands []string
18 | Timeout int64
19 | Err error
20 | LogContent []byte
21 | }
22 |
23 | type QueryJobArgs struct {
24 | SearchTxt string
25 | Root bool
26 | GroupID uint
27 | UserID uint
28 | Page, Pagesize int
29 | }
30 |
31 | type QueryCrontabJobRet struct {
32 | Total int64
33 | Page int
34 | GroupID uint
35 | Pagesize int
36 | List []models.CrontabJob
37 | }
38 |
39 | type QueryDaemonJobRet struct {
40 | Total int64
41 | GroupID int
42 | Page int
43 | Pagesize int
44 | List []models.DaemonJob
45 | }
46 |
47 | type AuditJobArgs struct {
48 | GroupID uint
49 | Root bool
50 | UserID uint
51 | JobIDs []uint
52 | }
53 |
54 | type CrontabApiNotifyBody struct {
55 | NodeAddr string
56 | JobName string
57 | JobID int
58 | CreateUsername string
59 | CreatedAt time.Time
60 | Timeout int64
61 | Type string
62 | RetryNum int
63 | }
64 |
65 | type EditCrontabJobArgs struct {
66 | Job models.CrontabJob
67 | UserID uint
68 | GroupID uint
69 | Root bool
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/proto/daemon.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import (
4 | "jiacrontab/models"
5 | )
6 |
7 | type EditDaemonJobArgs struct {
8 | Job models.DaemonJob
9 | GroupID uint
10 | UserID uint
11 | Root bool
12 | }
13 |
--------------------------------------------------------------------------------
/pkg/proto/resp.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | const (
4 | SuccessRespCode = 0
5 | ErrorRespCode = -1
6 | )
7 |
8 | type Resp struct {
9 | Code int `json:"code"`
10 | Msg string `json:"msg"`
11 | Data interface{} `json:"data,omitempty"`
12 | Sign string `json:"sign"`
13 | Version string `json:"version"`
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/rpc/client.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "jiacrontab/pkg/proto"
7 | "net"
8 | "net/rpc"
9 | "time"
10 |
11 | "github.com/iwannay/log"
12 | )
13 |
14 | const (
15 | diaTimeout = 5 * time.Second
16 | callTimeout = 1 * time.Minute
17 | pingDuration = 3 * time.Second
18 | )
19 |
20 | var (
21 | ErrRpc = errors.New("rpc is not available")
22 | ErrRpcTimeout = errors.New("rpc call timeout")
23 | ErrRpcCancel = errors.New("rpc call cancel")
24 | ErrShutdown = rpc.ErrShutdown
25 | )
26 |
27 | type ClientOptions struct {
28 | Network string
29 | Addr string
30 | }
31 |
32 | type Client struct {
33 | *rpc.Client
34 | options ClientOptions
35 | quit chan struct{}
36 | err error
37 | }
38 |
39 | func Dial(options ClientOptions) (c *Client) {
40 | c = &Client{}
41 | c.options = options
42 | c.dial()
43 | c.quit = make(chan struct{}, 100)
44 | return c
45 | }
46 |
47 | func (c *Client) dial() (err error) {
48 | conn, err := net.DialTimeout(c.options.Network, c.options.Addr, diaTimeout)
49 | if err != nil {
50 | return err
51 | }
52 | c.Client = rpc.NewClient(conn)
53 | return nil
54 | }
55 |
56 | func (c *Client) Call(serviceMethod string, ctx context.Context, args interface{}, reply interface{}) error {
57 | if serviceMethod != PingService && serviceMethod != RegisterService {
58 | log.Info("rpc call", c.options.Addr, serviceMethod)
59 | }
60 |
61 | if c.Client == nil {
62 | return ErrRpc
63 | }
64 | select {
65 | case <-ctx.Done():
66 | return ErrRpcCancel
67 | case call := <-c.Client.Go(serviceMethod, args, reply, make(chan *rpc.Call, 1)).Done:
68 | return call.Error
69 | case <-time.After(callTimeout):
70 | return ErrRpcTimeout
71 | }
72 | }
73 |
74 | func (c *Client) Error() error {
75 | return c.err
76 | }
77 |
78 | func (c *Client) Close() {
79 | c.quit <- struct{}{}
80 | }
81 |
82 | func (c *Client) Ping(serviceMethod string) {
83 | var (
84 | err error
85 | )
86 | for {
87 | select {
88 | case <-c.quit:
89 | goto closed
90 | default:
91 | }
92 | if c.Client != nil && c.err == nil {
93 | if err = c.Call(serviceMethod, context.TODO(), &proto.EmptyArgs{}, &proto.EmptyReply{}); err != nil {
94 | c.err = err
95 | c.Client.Close()
96 | log.Infof("client.Call(%s, args, reply) error (%v) \n", serviceMethod, err)
97 | }
98 | } else {
99 | if err = c.dial(); err == nil {
100 | c.err = nil
101 | log.Info("client reconnet ", c.options.Addr)
102 | }
103 | }
104 | time.Sleep(pingDuration)
105 | }
106 | closed:
107 | log.Info("rpc quited", c.options.Addr)
108 | if c.Client != nil {
109 | c.Client.Close()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/pkg/rpc/client_test.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "jiacrontab/pkg/proto"
5 | "log"
6 | "net/http"
7 | _ "net/http/pprof"
8 | "sync"
9 | "testing"
10 | "time"
11 | )
12 |
13 | type Logic struct {
14 | }
15 |
16 | func (l *Logic) Ping(args *proto.EmptyArgs, reply *proto.EmptyReply) error {
17 | return nil
18 | }
19 |
20 | func (p *Logic) Say(args string, reply *string) error {
21 |
22 | *reply = "hello boy"
23 | time.Sleep(100 * time.Second)
24 | return nil
25 | }
26 | func TestCall(t *testing.T) {
27 | done := make(chan struct{})
28 | go func() {
29 |
30 | done <- struct{}{}
31 |
32 | log.Println("start server")
33 | err := listen(":6478", &Logic{})
34 | if err != nil {
35 | t.Fatal("server error:", err)
36 | }
37 | }()
38 | <-done
39 | time.Sleep(5 * time.Second)
40 | // 等待server启动
41 | var wg sync.WaitGroup
42 | for i := 0; i < 100; i++ {
43 | wg.Add(1)
44 | go func(i int) {
45 |
46 | defer wg.Done()
47 | var ret string
48 | // var args string
49 | err := Call(":6478", "Logic.Say", "", &ret)
50 | if err != nil {
51 | log.Println(i, "error:", err)
52 | }
53 | t.Log(i, ret)
54 | }(i)
55 |
56 | }
57 |
58 | go func() {
59 | t.Log("listen :6060")
60 | t.Log(http.ListenAndServe(":6060", nil))
61 | }()
62 |
63 | wg.Wait()
64 | log.Println("end")
65 | time.Sleep(2 * time.Minute)
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/rpc/clients.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 | "net/rpc"
6 | "sync"
7 |
8 | "github.com/iwannay/log"
9 | )
10 |
11 | var (
12 | defaultClients *clients
13 | PingService = "Srv.Ping"
14 | RegisterService = "Srv.Register"
15 | )
16 |
17 | type clients struct {
18 | lock sync.RWMutex
19 | clients map[string]*Client
20 | }
21 |
22 | func (c *clients) get(addr string) *Client {
23 | var (
24 | cli *Client
25 | ok bool
26 | op ClientOptions
27 | )
28 |
29 | c.lock.Lock()
30 | defer c.lock.Unlock()
31 | if cli, ok = c.clients[addr]; ok {
32 | return cli
33 | }
34 | op.Network = "tcp4"
35 | op.Addr = addr
36 | cli = Dial(op)
37 | c.clients[addr] = cli
38 | go cli.Ping(PingService)
39 |
40 | return cli
41 | }
42 |
43 | func (c *clients) del(addr string) {
44 | c.lock.Lock()
45 | defer c.lock.Unlock()
46 | if cli, ok := c.clients[addr]; ok {
47 | cli.Close()
48 | }
49 | delete(c.clients, addr)
50 | }
51 |
52 | func Call(addr string, serviceMethod string, args interface{}, reply interface{}) error {
53 | err := defaultClients.get(addr).Call(serviceMethod, context.TODO(), args, reply)
54 | if err == rpc.ErrShutdown {
55 | log.Debug("rpc remove", addr)
56 | Del(addr)
57 | }
58 | return err
59 | }
60 |
61 | func CallCtx(addr string, serviceMethod string, ctx context.Context, args interface{}, reply interface{}) error {
62 | err := defaultClients.get(addr).Call(serviceMethod, ctx, args, reply)
63 | if err == rpc.ErrShutdown {
64 | log.Debug("rpc remove", addr)
65 | Del(addr)
66 | }
67 | return err
68 | }
69 |
70 | func Del(addr string) {
71 | if defaultClients != nil {
72 | defaultClients.del(addr)
73 | }
74 | }
75 |
76 | func DelNode(addr string) {
77 | if defaultClients != nil {
78 | defaultClients.del(addr)
79 | }
80 | }
81 |
82 | func init() {
83 | defaultClients = &clients{
84 | clients: make(map[string]*Client),
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/rpc/server.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "github.com/iwannay/log"
5 | "net"
6 | "net/rpc"
7 | )
8 |
9 | // listen Start rpc server
10 | func listen(addr string, srcvr ...interface{}) error {
11 | var err error
12 | for _, v := range srcvr {
13 | if err = rpc.Register(v); err != nil {
14 | return err
15 | }
16 | }
17 |
18 | l, err := net.Listen("tcp4", addr)
19 | if err != nil {
20 | return err
21 | }
22 | defer func() {
23 | log.Info("listen rpc", addr, "close")
24 | if err := l.Close(); err != nil {
25 | log.Infof("listen.Close() error(%v)", err)
26 | }
27 | }()
28 |
29 | rpc.Accept(l)
30 | return nil
31 | }
32 |
33 | // ListenAndServe run rpc server
34 | func ListenAndServe(addr string, srcvr ...interface{}) {
35 | log.Info("rpc server listen:", addr)
36 | err := listen(addr, srcvr...)
37 | if err != nil {
38 | panic(err)
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/test/assertions.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "path/filepath"
5 | "reflect"
6 | "runtime"
7 | "testing"
8 | )
9 |
10 | func Equal(t *testing.T, expected, actual interface{}) {
11 | if !reflect.DeepEqual(expected, actual) {
12 | _, file, line, _ := runtime.Caller(1)
13 | t.Logf("\033[31m%s:%d:\n\n\t %#v (expected)\n\n\t!= %#v (actual)\033[39m\n\n",
14 | filepath.Base(file), line, expected, actual)
15 | t.FailNow()
16 | }
17 | }
18 |
19 | func NotEqual(t *testing.T, expected, actual interface{}) {
20 | if reflect.DeepEqual(expected, actual) {
21 | _, file, line, _ := runtime.Caller(1)
22 | t.Logf("\033[31m%s:%d:\n\n\tnexp: %#v\n\n\tgot: %#v\033[39m\n\n",
23 | filepath.Base(file), line, expected, actual)
24 | t.FailNow()
25 | }
26 | }
27 |
28 | func Nil(t *testing.T, object interface{}) {
29 | if !isNil(object) {
30 | _, file, line, _ := runtime.Caller(1)
31 | t.Logf("\033[31m%s:%d:\n\n\t (expected)\n\n\t!= %#v (actual)\033[39m\n\n",
32 | filepath.Base(file), line, object)
33 | t.FailNow()
34 | }
35 | }
36 |
37 | func NotNil(t *testing.T, object interface{}) {
38 | if isNil(object) {
39 | _, file, line, _ := runtime.Caller(1)
40 | t.Logf("\033[31m%s:%d:\n\n\tExpected value not to be \033[39m\n\n",
41 | filepath.Base(file), line)
42 | t.FailNow()
43 | }
44 | }
45 |
46 | func isNil(object interface{}) bool {
47 | if object == nil {
48 | return true
49 | }
50 |
51 | value := reflect.ValueOf(object)
52 | kind := value.Kind()
53 | if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() {
54 | return true
55 | }
56 |
57 | return false
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/test/fakes.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "net"
5 | "time"
6 | )
7 |
8 | type FakeNetConn struct {
9 | ReadFunc func([]byte) (int, error)
10 | WriteFunc func([]byte) (int, error)
11 | CloseFunc func() error
12 | LocalAddrFunc func() net.Addr
13 | RemoteAddrFunc func() net.Addr
14 | SetDeadlineFunc func(time.Time) error
15 | SetReadDeadlineFunc func(time.Time) error
16 | SetWriteDeadlineFunc func(time.Time) error
17 | }
18 |
19 | func (f FakeNetConn) Read(b []byte) (int, error) { return f.ReadFunc(b) }
20 | func (f FakeNetConn) Write(b []byte) (int, error) { return f.WriteFunc(b) }
21 | func (f FakeNetConn) Close() error { return f.CloseFunc() }
22 | func (f FakeNetConn) LocalAddr() net.Addr { return f.LocalAddrFunc() }
23 | func (f FakeNetConn) RemoteAddr() net.Addr { return f.RemoteAddrFunc() }
24 | func (f FakeNetConn) SetDeadline(t time.Time) error { return f.SetDeadlineFunc(t) }
25 | func (f FakeNetConn) SetReadDeadline(t time.Time) error { return f.SetReadDeadlineFunc(t) }
26 | func (f FakeNetConn) SetWriteDeadline(t time.Time) error { return f.SetWriteDeadlineFunc(t) }
27 |
28 | type fakeNetAddr struct{}
29 |
30 | func (fakeNetAddr) Network() string { return "" }
31 | func (fakeNetAddr) String() string { return "" }
32 |
33 | func NewFakeNetConn() FakeNetConn {
34 | netAddr := fakeNetAddr{}
35 | return FakeNetConn{
36 | ReadFunc: func(b []byte) (int, error) { return 0, nil },
37 | WriteFunc: func(b []byte) (int, error) { return len(b), nil },
38 | CloseFunc: func() error { return nil },
39 | LocalAddrFunc: func() net.Addr { return netAddr },
40 | RemoteAddrFunc: func() net.Addr { return netAddr },
41 | SetDeadlineFunc: func(time.Time) error { return nil },
42 | SetWriteDeadlineFunc: func(time.Time) error { return nil },
43 | SetReadDeadlineFunc: func(time.Time) error { return nil },
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/test/logger.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | type Logger interface {
4 | Output(maxdepth int, s string) error
5 | }
6 |
7 | type tbLog interface {
8 | Log(...interface{})
9 | }
10 |
11 | type testLogger struct {
12 | tbLog
13 | }
14 |
15 | func (tl *testLogger) Output(maxdepth int, s string) error {
16 | tl.Log(s)
17 | return nil
18 | }
19 |
20 | func NewTestLogger(tbl tbLog) Logger {
21 | return &testLogger{tbl}
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/util/arr.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | func FilterEmptyEle(in []string) (out []string) {
4 | for _, v := range in {
5 | if v != "" {
6 | out = append(out, v)
7 | }
8 | }
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/util/fn.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "jiacrontab/pkg/file"
5 | "reflect"
6 | "strconv"
7 |
8 | "github.com/gofrs/uuid"
9 |
10 | "fmt"
11 | "io/ioutil"
12 | "math/rand"
13 |
14 | "github.com/iwannay/log"
15 |
16 | "flag"
17 | "os"
18 | "path/filepath"
19 | "runtime"
20 | "time"
21 | )
22 |
23 | func RandIntn(end int) int {
24 | return rand.Intn(end)
25 | }
26 |
27 | func CurrentTime(t int64) string {
28 | if t == 0 {
29 | return "0"
30 | }
31 | return time.Unix(t, 0).Format("2006-01-02 15:04:05")
32 | }
33 |
34 | func SystemInfo(startTime time.Time) map[string]interface{} {
35 | var afterLastGC string
36 | goNum := runtime.NumGoroutine()
37 | cpuNum := runtime.NumCPU()
38 | mstat := &runtime.MemStats{}
39 | runtime.ReadMemStats(mstat)
40 | costTime := int(time.Since(startTime).Seconds())
41 |
42 | if mstat.LastGC != 0 {
43 | afterLastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(mstat.LastGC))/1000/1000/1000)
44 | } else {
45 | afterLastGC = "0"
46 | }
47 |
48 | return map[string]interface{}{
49 | "服务运行时间": fmt.Sprintf("%d天%d小时%d分%d秒", costTime/(3600*24), costTime%(3600*24)/3600, costTime%3600/60, costTime%(60)),
50 | "goroutine数量": goNum,
51 | "cpu核心数": cpuNum,
52 |
53 | "当前内存使用量": file.FileSize(int64(mstat.Alloc)),
54 | "所有被分配的内存": file.FileSize(int64(mstat.TotalAlloc)),
55 | "内存占用量": file.FileSize(int64(mstat.Sys)),
56 | "指针查找次数": mstat.Lookups,
57 | "内存分配次数": mstat.Mallocs,
58 | "内存释放次数": mstat.Frees,
59 | "距离上次GC时间": afterLastGC,
60 |
61 | // "当前 Heap 内存使用量": file.FileSize(int64(mstat.HeapAlloc)),
62 | // "Heap 内存占用量": file.FileSize(int64(mstat.HeapSys)),
63 | // "Heap 内存空闲量": file.FileSize(int64(mstat.HeapIdle)),
64 | // "正在使用的 Heap 内存": file.FileSize(int64(mstat.HeapInuse)),
65 | // "被释放的 Heap 内存": file.FileSize(int64(mstat.HeapReleased)),
66 | // "Heap 对象数量": mstat.HeapObjects,
67 |
68 | "下次GC内存回收量": file.FileSize(int64(mstat.NextGC)),
69 | "GC暂停时间总量": fmt.Sprintf("%.3fs", float64(mstat.PauseTotalNs)/1000/1000/1000),
70 | "上次GC暂停时间": fmt.Sprintf("%.3fs", float64(mstat.PauseNs[(mstat.NumGC+255)%256])/1000/1000/1000),
71 | }
72 | }
73 |
74 | func TryOpen(path string, flag int) (*os.File, error) {
75 | fabs, err := filepath.Abs(path)
76 | if err != nil {
77 | log.Errorf("TryOpen:", err)
78 | return nil, err
79 | }
80 |
81 | f, err := os.OpenFile(fabs, flag, 0644)
82 | if os.IsNotExist(err) {
83 | err = os.MkdirAll(filepath.Dir(fabs), 0755)
84 | if err != nil {
85 | return nil, err
86 | }
87 | return os.OpenFile(fabs, flag, 0644)
88 | }
89 | return f, err
90 | }
91 |
92 | func CatFile(filepath string, limit int64, content *string) (isPath bool, err error) {
93 | f, err := os.Open(filepath)
94 |
95 | if err != nil {
96 | return false, err
97 | }
98 | defer f.Close()
99 | fi, err := f.Stat()
100 | if err != nil {
101 | return false, err
102 | }
103 |
104 | if fi.Size() > limit {
105 | *content = filepath
106 | return true, nil
107 | }
108 | data, err := ioutil.ReadAll(f)
109 | if err != nil {
110 | return false, err
111 | }
112 | *content = string(data)
113 | return false, nil
114 | }
115 |
116 | func ParseInt(i string) int {
117 | v, _ := strconv.Atoi(i)
118 | return v
119 | }
120 |
121 | func ParseInt64(i string) int64 {
122 | v, _ := strconv.Atoi(i)
123 | return int64(v)
124 | }
125 |
126 | func InArray(val interface{}, arr interface{}) bool {
127 | t := reflect.TypeOf(arr)
128 | v := reflect.ValueOf(arr)
129 |
130 | if t.Kind() == reflect.Slice {
131 | for i := 0; i < v.Len(); i++ {
132 | if v.Index(i).Interface() == val {
133 | return true
134 | }
135 | }
136 | }
137 |
138 | return false
139 | }
140 |
141 | func UUID() string {
142 |
143 | uu, err := uuid.NewGen().NewV1()
144 |
145 | if err != nil {
146 | log.Error(err)
147 | return fmt.Sprint(time.Now().UnixNano())
148 | }
149 |
150 | return uu.String()
151 | }
152 |
153 | func GetHostname() string {
154 | hostname, err := os.Hostname()
155 | if err != nil {
156 | log.Error("GetHostname:", err)
157 | }
158 | return hostname
159 | }
160 |
161 | func HasFlagName(fs *flag.FlagSet, s string) bool {
162 | var found bool
163 | fs.Visit(func(flag *flag.Flag) {
164 | if flag.Name == s {
165 | found = true
166 | }
167 | })
168 | return found
169 |
170 | }
171 |
172 | func init() {
173 | rand.Seed(time.Now().UnixNano())
174 | }
175 |
--------------------------------------------------------------------------------
/pkg/util/ip.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "net"
5 | "strings"
6 | )
7 |
8 | // InternalIP return internal ip.
9 | func InternalIP() string {
10 | inters, err := net.Interfaces()
11 | if err != nil {
12 | return ""
13 | }
14 | for _, inter := range inters {
15 | if inter.Flags&net.FlagUp != 0 && !strings.HasPrefix(inter.Name, "lo") {
16 | addrs, err := inter.Addrs()
17 | if err != nil {
18 | continue
19 | }
20 | for _, addr := range addrs {
21 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
22 | if ipnet.IP.To4() != nil {
23 | return ipnet.IP.String()
24 | }
25 | }
26 | }
27 | }
28 | }
29 | return ""
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/util/time.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | func CountDaysOfMonth(year int, month int) (days int) {
4 | if month != 2 {
5 | if month == 4 || month == 6 || month == 9 || month == 11 {
6 | days = 30
7 | } else {
8 | days = 31
9 | }
10 | } else {
11 | if ((year%4) == 0 && (year%100) != 0) || (year%400) == 0 {
12 | days = 29
13 | } else {
14 | days = 28
15 | }
16 | }
17 | return
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/util/wait_group_wrapper.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type WaitGroupWrapper struct {
8 | sync.WaitGroup
9 | }
10 |
11 | func (w *WaitGroupWrapper) Wrap(cb func()) {
12 | w.Add(1)
13 | go func() {
14 | cb()
15 | w.Done()
16 | }()
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/version/ver.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | )
7 |
8 | var Binary string
9 |
10 | func String(app string) string {
11 | return fmt.Sprintf("%s v%s (built w/%s)", app, Binary, runtime.Version())
12 | }
13 |
--------------------------------------------------------------------------------
/qq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iwannay/jiacrontab/556339ada56bf88d00796c3291b5e71bb547abd2/qq.png
--------------------------------------------------------------------------------