├── .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 | [![Build Status](https://travis-ci.org/iwannay/jiacrontab.svg?branch=dev)](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 | 赞助qq群 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%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 --------------------------------------------------------------------------------