mirror of https://github.com/shunfei/cronsun
导入任务,并增加ls命令,可以列出当前node
commit
a18f5ac3fc
26
.travis.yml
26
.travis.yml
|
@ -1,20 +1,16 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.9
|
||||
- 1.11.x
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
install:
|
||||
- go get -u github.com/coreos/etcd/clientv3
|
||||
- go get github.com/rogpeppe/fastuuid
|
||||
- go get gopkg.in/mgo.v2
|
||||
- go get github.com/fsnotify/fsnotify
|
||||
- go get github.com/go-gomail/gomail
|
||||
- go get go.uber.org/zap
|
||||
- go get github.com/cockroachdb/cmux
|
||||
- go get github.com/gorilla/mux
|
||||
- go get github.com/smartystreets/goconvey/convey
|
||||
- go get github.com/spf13/cobra
|
||||
- go get github.com/satori/go.uuid
|
||||
- go mod vendor
|
||||
|
||||
before_script:
|
||||
- go vet -x ./...
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
- go vet -v $(go list ./... | grep -v vendor)
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: "1.11.x"
|
||||
script: go test -v -race -mod=vendor ./...
|
||||
|
||||
|
|
10
README.md
10
README.md
|
@ -12,7 +12,7 @@ The goal of this project is to make it much easier to manage jobs on lots of mac
|
|||
## Features
|
||||
|
||||
- Easy manage jobs on multiple machines
|
||||
- Managemant panel
|
||||
- Management panel
|
||||
- Mail service
|
||||
- Multi-language support
|
||||
- Simple authentication and accounts manager(default administrator email and password: admin@admin.com/admin)
|
||||
|
@ -66,13 +66,14 @@ We encourage you to try it, it's easy to use, see how it works for you. We belie
|
|||
|
||||
Install from binary [latest release](https://github.com/shunfei/cronsun/releases/latest)
|
||||
|
||||
Or build from source ([feature/glide](https://github.com/shunfei/cronsun/tree/feature/glide)), require `go >= 1.9+`, [glide](https://glide.sh/)
|
||||
Or build from source, require `go >= 1.11+`.
|
||||
> NOTE: The branch `master` is not in stable, using Cronsun for production please checkout corresponding tags.
|
||||
|
||||
```
|
||||
export GO111MODULE=on
|
||||
go get -u github.com/shunfei/cronsun
|
||||
cd $GOPATH/src/github.com/shunfei/cronsun
|
||||
git checkout feature/glide
|
||||
glide update
|
||||
go mod vendor
|
||||
sh build.sh
|
||||
```
|
||||
|
||||
|
@ -83,6 +84,7 @@ sh build.sh
|
|||
3. Open and update Etcd(`conf/etcd.json`) and MongoDB(`conf/db.json`) configurations
|
||||
4. Start cronnode: `./cronnode -conf conf/base.json`, start cronweb: `./cronweb -conf conf/base.json`
|
||||
5. Open `http://127.0.0.1:7079` in browser
|
||||
6. Login with username `admin@admin.com` and password `admin`
|
||||
|
||||
## Screenshot
|
||||
|
||||
|
|
19
README_ZH.md
19
README_ZH.md
|
@ -10,6 +10,20 @@
|
|||
|
||||
`cronsun`已经在线上几百台规模的服务器上面稳定运行了一年多了,虽然目前版本不是正式版,但是我们认为是完全可以用于生产环境的。强烈建议你试用下,因为它非常简单易用,同时感受下他的强大,相信你会喜欢上这个工具的。
|
||||
|
||||
|
||||
## 特性
|
||||
|
||||
- 方便对多台服务器上面的定时任务进行集中式管理
|
||||
- 任务调度时间粒度支持到`秒`级别
|
||||
- 任务失败自动重试
|
||||
- 任务可靠性保障(从N个节点里面挑一个可用节点来执行任务)
|
||||
- 简洁易用的管理后台,支持多语言
|
||||
- 任务日志查看
|
||||
- 任务失败邮件告警(也支持自定义http告警接口)
|
||||
- 用户验证与授权 (默认账号密码: admin@admin.com / admin)
|
||||
- [可靠性说明](https://github.com/shunfei/cronsun/wiki/%E5%8F%AF%E9%9D%A0%E6%80%A7%E8%AF%B4%E6%98%8E)
|
||||
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
|
@ -55,11 +69,13 @@
|
|||
|
||||
直接下载执行文件 [latest release](https://github.com/shunfei/cronsun/releases/latest)。
|
||||
|
||||
如果你熟悉 `Go`,也可以从源码编译, 要求 `go >= 1.9+`
|
||||
如果你熟悉 `Go`,也可以从源码编译, 要求 `go >= 1.11+`
|
||||
|
||||
```
|
||||
go get -u github.com/shunfei/cronsun
|
||||
cd $GOPATH/src/github.com/shunfei/cronsun
|
||||
go mod vendor
|
||||
# 如果 go mod vendor 下载失败,请尝试 https://goproxy.io
|
||||
sh build.sh
|
||||
```
|
||||
|
||||
|
@ -70,6 +86,7 @@ sh build.sh
|
|||
3. 修改 `conf` 相关的配置
|
||||
4. 在任务结点启动 `./cronnode -conf conf/base.json`,在管理结点启动 `./cronweb -conf conf/base.json`
|
||||
5. 访问管理界面 `http://127.0.0.1:7079/ui/`
|
||||
6. 使用用户名 `admin@admin.com` 和密码 `admin` 进行登录
|
||||
|
||||
### 关于后台权限
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ func main() {
|
|||
lcf := zap.NewDevelopmentConfig()
|
||||
lcf.Level.SetLevel(zapcore.Level(*level))
|
||||
lcf.Development = false
|
||||
lcf.DisableStacktrace = true
|
||||
logger, err := lcf.Build(zap.AddCallerSkip(1))
|
||||
if err != nil {
|
||||
slog.Fatalln("new log err:", err.Error())
|
||||
|
|
|
@ -120,7 +120,7 @@ func (c *Client) DelLock(key string) error {
|
|||
}
|
||||
|
||||
func IsValidAsKeyPath(s string) bool {
|
||||
return strings.IndexByte(s, '/') == -1
|
||||
return strings.IndexAny(s, "/\\") == -1
|
||||
}
|
||||
|
||||
// etcdTimeoutContext return better error info
|
||||
|
|
10
conf/conf.go
10
conf/conf.go
|
@ -2,6 +2,7 @@ package conf
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -11,7 +12,7 @@ import (
|
|||
client "github.com/coreos/etcd/clientv3"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/go-gomail/gomail"
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/shunfei/cronsun/db"
|
||||
"github.com/shunfei/cronsun/event"
|
||||
|
@ -159,7 +160,8 @@ func (c *Conf) UUID() (string, error) {
|
|||
if len(b) == 0 {
|
||||
return c.genUUID()
|
||||
}
|
||||
return string(b), nil
|
||||
suid := strings.Join(strings.Fields(string(b)), "")
|
||||
return suid, nil
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
|
@ -177,12 +179,12 @@ func (c *Conf) genUUID() (string, error) {
|
|||
|
||||
uuidDir := path.Dir(c.UUIDFile)
|
||||
if err := os.MkdirAll(uuidDir, 0755); err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("failed to write UUID to file: %s. you can change UUIDFile config in base.json", err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(c.UUIDFile, []byte(u.String()), 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("failed to write UUID to file: %s. you can change UUIDFile config in base.json", err)
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
|
|
|
@ -24,6 +24,6 @@
|
|||
"Security": "@extend:security.json",
|
||||
"#comment": "PIDFile and UUIDFile just work for cronnode",
|
||||
"#PIDFile": "Given a none-empty string to write a pid file to the specialed path, or leave it empty to do nothing",
|
||||
"PIDFile": "/tmp/cronsun/cronnode_pid",
|
||||
"PIDFile": "/var/run/cronsun/cronnode.pid",
|
||||
"UUIDFile": "/etc/cronsun/CRONSUN_UUID"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
"127.0.0.1:27017"
|
||||
],
|
||||
"Database": "cronsun",
|
||||
"UserName": "",
|
||||
"Password": "",
|
||||
"#Timeout": "connect timeout duration/second",
|
||||
"Timeout": 15
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"BindAddr": ":7079",
|
||||
"Auth": {
|
||||
"Enabled": false
|
||||
"#Enabled": "set to true to open auth. default username and password is admin@admin.com/admin",
|
||||
"Enabled": true
|
||||
},
|
||||
"Session": {
|
||||
"StorePrefixPath": "/cronsun/sess/",
|
||||
"CookieName": "uid",
|
||||
"CookieName": "cronsun_uid",
|
||||
"Expiration": 8640000
|
||||
},
|
||||
"#comment": "Delete the expired log (which store in mongodb) periodically",
|
||||
|
|
|
@ -8,8 +8,8 @@ var (
|
|||
|
||||
ErrEmptyJobName = errors.New("Name of job is empty.")
|
||||
ErrEmptyJobCommand = errors.New("Command of job is empty.")
|
||||
ErrIllegalJobId = errors.New("Invalid id that includes illegal characters such as '/'.")
|
||||
ErrIllegalJobGroupName = errors.New("Invalid job group name that includes illegal characters such as '/'.")
|
||||
ErrIllegalJobId = errors.New("Invalid id that includes illegal characters such as '/' '\\'.")
|
||||
ErrIllegalJobGroupName = errors.New("Invalid job group name that includes illegal characters such as '/' '\\'.")
|
||||
|
||||
ErrEmptyNodeGroupName = errors.New("Name of node group is empty.")
|
||||
ErrIllegalNodeGroupId = errors.New("Invalid node group id that includes illegal characters such as '/'.")
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
module github.com/shunfei/cronsun
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292
|
||||
github.com/coreos/bbolt v1.3.0 // indirect
|
||||
github.com/coreos/etcd v3.3.9+incompatible
|
||||
github.com/coreos/go-semver v0.2.0 // indirect
|
||||
github.com/coreos/go-systemd v0.0.0-20180828140353-eee3db372b31 // indirect
|
||||
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||
github.com/gofrs/uuid v3.1.0+incompatible
|
||||
github.com/gogo/protobuf v1.1.1 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 // indirect
|
||||
github.com/golang/protobuf v1.2.0 // indirect
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2
|
||||
github.com/gorilla/websocket v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.4.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v0.8.0 // indirect
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 // indirect
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af
|
||||
github.com/sirupsen/logrus v1.0.6 // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a
|
||||
github.com/soheilhy/cmux v0.1.4 // indirect
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/pflag v1.0.2 // indirect
|
||||
github.com/stretchr/testify v1.2.2 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
|
||||
github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.9.1
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d // indirect
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b // indirect
|
||||
google.golang.org/grpc v1.14.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
|
||||
)
|
|
@ -0,0 +1,112 @@
|
|||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292 h1:dzj1/xcivGjNPwwifh/dWTczkwcuqsXXFHY1X/TZMtw=
|
||||
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
|
||||
github.com/coreos/bbolt v1.3.0 h1:HIgH5xUWXT914HCI671AxuTTqjj64UOFr7pHn48LUTI=
|
||||
github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.9+incompatible h1:/pWnp1yEff0z+vBEOBFLZZ22Ux5xoVozEe7X0VFyRNo=
|
||||
github.com/coreos/etcd v3.3.9+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180828140353-eee3db372b31 h1:wRzCUSYhBIk1KvRIlx+nvScCRIxX0iIhSU5h9xj7MUU=
|
||||
github.com/coreos/go-systemd v0.0.0-20180828140353-eee3db372b31/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea h1:n2Ltr3SrfQlf/9nOna1DoGKxLx3qTSI8Ttl6Xrqp6mw=
|
||||
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 h1:u4bArs140e9+AfE52mFHOXVFnOSBJBRlzTHrOPLOIhE=
|
||||
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.4.1 h1:pX7cnDwSSmG0dR9yNjCQSSpmsJOqFdT7SzVp5Yl9uVw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co=
|
||||
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942 h1:CZORS/4d6i+5FKSAtbRIjlElV2BAFYv/bokcaEVUimQ=
|
||||
github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 h1:MPPkRncZLN9Kh4MEFmbnK4h3BD7AUmskWv2+EeZJCCs=
|
||||
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
82
job.go
82
job.go
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
|
@ -410,37 +411,41 @@ func (j *Job) String() string {
|
|||
return string(data)
|
||||
}
|
||||
|
||||
// GetNextRunTime return the job's next run time by now,
|
||||
// will return zero time if job will not run.
|
||||
func (j *Job) GetNextRunTime() time.Time {
|
||||
nextTime := time.Time{}
|
||||
if len(j.Rules) < 1 {
|
||||
return nextTime
|
||||
}
|
||||
for i, r := range j.Rules {
|
||||
sch, err := cron.Parse(r.Timer)
|
||||
if err != nil {
|
||||
return nextTime
|
||||
}
|
||||
t := sch.Next(time.Now())
|
||||
if i == 0 || t.UnixNano() < nextTime.UnixNano() {
|
||||
nextTime = t
|
||||
}
|
||||
}
|
||||
return nextTime
|
||||
}
|
||||
|
||||
// Run 执行任务
|
||||
func (j *Job) Run() bool {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
proc *Process
|
||||
sysProcAttr *syscall.SysProcAttr
|
||||
err error
|
||||
)
|
||||
|
||||
t := time.Now()
|
||||
// 用户权限控制
|
||||
if len(j.User) > 0 {
|
||||
u, err := user.Lookup(j.User)
|
||||
if err != nil {
|
||||
j.Fail(t, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
if err != nil {
|
||||
j.Fail(t, "not support run with user on windows")
|
||||
return false
|
||||
}
|
||||
if uid != _Uid {
|
||||
gid, _ := strconv.Atoi(u.Gid)
|
||||
sysProcAttr = &syscall.SysProcAttr{
|
||||
Credential: &syscall.Credential{
|
||||
Uid: uint32(uid),
|
||||
Gid: uint32(gid),
|
||||
},
|
||||
}
|
||||
}
|
||||
sysProcAttr, err = j.CreateCmdAttr()
|
||||
if err != nil {
|
||||
j.Fail(t, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// 超时控制
|
||||
|
@ -451,6 +456,7 @@ func (j *Job) Run() bool {
|
|||
} else {
|
||||
cmd = exec.Command(j.cmd[0], j.cmd[1:]...)
|
||||
}
|
||||
|
||||
cmd.SysProcAttr = sysProcAttr
|
||||
var b bytes.Buffer
|
||||
cmd.Stdout = &b
|
||||
|
@ -465,7 +471,9 @@ func (j *Job) Run() bool {
|
|||
JobID: j.ID,
|
||||
Group: j.Group,
|
||||
NodeID: j.runOn,
|
||||
Time: t,
|
||||
ProcessVal: ProcessVal{
|
||||
Time: t,
|
||||
},
|
||||
}
|
||||
proc.Start()
|
||||
defer proc.Stop()
|
||||
|
@ -722,3 +730,33 @@ func (j *Job) ShortName() string {
|
|||
|
||||
return string(names[:10]) + "..."
|
||||
}
|
||||
|
||||
func (j *Job) CreateCmdAttr() (*syscall.SysProcAttr, error) {
|
||||
sysProcAttr := &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
if len(j.User) == 0 {
|
||||
return sysProcAttr, nil
|
||||
}
|
||||
|
||||
u, err := user.Lookup(j.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
if err != nil {
|
||||
return nil, errors.New("not support run with user on windows")
|
||||
}
|
||||
|
||||
if uid != _Uid {
|
||||
gid, _ := strconv.Atoi(u.Gid)
|
||||
sysProcAttr.Credential = &syscall.Credential{
|
||||
Uid: uint32(uid),
|
||||
Gid: uint32(gid),
|
||||
}
|
||||
}
|
||||
|
||||
return sysProcAttr, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TimeListSchedule will run at the specify giving time.
|
||||
type TimeListSchedule struct {
|
||||
timeList []time.Time
|
||||
}
|
||||
|
||||
// At returns a crontab Schedule that activates every specify time.
|
||||
func At(tl []time.Time) *TimeListSchedule {
|
||||
sort.Slice(tl, func(i, j int) bool { return tl[i].Unix() < tl[j].Unix() })
|
||||
return &TimeListSchedule{
|
||||
timeList: tl,
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule *TimeListSchedule) Next(t time.Time) time.Time {
|
||||
cur := 0
|
||||
for cur < len(schedule.timeList) {
|
||||
nextt := schedule.timeList[cur]
|
||||
cur++
|
||||
if nextt.UnixNano() <= t.UnixNano() {
|
||||
continue
|
||||
}
|
||||
return nextt
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimeListNext(t *testing.T) {
|
||||
tests := []struct {
|
||||
startTime string
|
||||
times []string
|
||||
expected []string
|
||||
}{
|
||||
// Simple cases
|
||||
{
|
||||
"2018-09-01 08:01:02",
|
||||
[]string{"2018-09-01 10:01:02"},
|
||||
[]string{"2018-09-01 10:01:02"},
|
||||
},
|
||||
|
||||
// sort list
|
||||
{
|
||||
"2018-09-01 08:01:02",
|
||||
[]string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"},
|
||||
[]string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"},
|
||||
},
|
||||
|
||||
// sort list with middle start time
|
||||
{
|
||||
"2018-09-01 10:11:02",
|
||||
[]string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"},
|
||||
[]string{"2018-09-02 10:01:02"},
|
||||
},
|
||||
|
||||
// unsorted list
|
||||
{
|
||||
"2018-07-01 08:01:02",
|
||||
[]string{"2018-09-01 10:01:00", "2018-08-01 10:00:00", "2018-09-01 10:00:00", "2018-08-02 10:01:02"},
|
||||
[]string{"2018-08-01 10:00:00", "2018-08-02 10:01:02", "2018-09-01 10:00:00", "2018-09-01 10:01:00"},
|
||||
},
|
||||
|
||||
// unsorted list with middle start time
|
||||
{
|
||||
"2018-08-03 12:00:00",
|
||||
[]string{"2018-09-01 10:01:00", "2018-08-01 10:00:00", "2018-09-01 10:00:00", "2018-08-02 10:01:02"},
|
||||
[]string{"2018-09-01 10:00:00", "2018-09-01 10:01:00"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range tests {
|
||||
tls := At(getAtTimes(c.times))
|
||||
nextTime := getAtTime(c.startTime)
|
||||
for _, trun := range c.expected {
|
||||
actual := tls.Next(nextTime)
|
||||
expected := getAtTime(trun)
|
||||
if actual != expected {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)",
|
||||
c.startTime, c.times, expected, actual)
|
||||
}
|
||||
nextTime = actual
|
||||
}
|
||||
if actual := tls.Next(nextTime); !actual.IsZero() {
|
||||
t.Errorf("%s, \"%s\": next time should be zero, but got %v (actual)",
|
||||
c.startTime, c.times, actual)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func getAtTime(value string) time.Time {
|
||||
if value == "" {
|
||||
panic("time string is empty")
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02 15:04:05", value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func getAtTimes(values []string) []time.Time {
|
||||
tl := []time.Time{}
|
||||
for _, v := range values {
|
||||
tl = append(tl, getAtTime(v))
|
||||
}
|
||||
return tl
|
||||
}
|
|
@ -373,5 +373,21 @@ func parseDescriptor(descriptor string) (Schedule, error) {
|
|||
return Every(duration), nil
|
||||
}
|
||||
|
||||
const at = "@at "
|
||||
if strings.HasPrefix(descriptor, at) {
|
||||
tss := strings.Split(descriptor[len(at):], ",")
|
||||
atls := make([]time.Time, 0, len(tss))
|
||||
for _, ts := range tss {
|
||||
ts = strings.TrimSpace(ts)
|
||||
att, err := time.ParseInLocation("2006-01-02 15:04:05", ts, time.Local)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse time %s: %s", descriptor, err)
|
||||
}
|
||||
atls = append(atls, att)
|
||||
}
|
||||
|
||||
return At(atls), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
|
||||
}
|
||||
|
|
45
node/node.go
45
node/node.go
|
@ -1,16 +1,17 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
client "github.com/coreos/etcd/clientv3"
|
||||
|
||||
"github.com/shunfei/cronsun"
|
||||
"github.com/shunfei/cronsun/conf"
|
||||
"github.com/shunfei/cronsun/log"
|
||||
|
@ -120,14 +121,14 @@ func (n *Node) writePIDFile() {
|
|||
dir := path.Dir(n.PIDFile)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to write pid file: %s", err)
|
||||
log.Errorf("Failed to write pid file: %s. you can change PIDFile config in base.json", err)
|
||||
return
|
||||
}
|
||||
|
||||
n.PIDFile = path.Join(dir, filename)
|
||||
err = ioutil.WriteFile(n.PIDFile, []byte(n.PID), 0644)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to write pid file: %s", err)
|
||||
log.Errorf("Failed to write pid file: %s. you can change PIDFile config in base.json", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -420,6 +421,14 @@ func (n *Node) groupRmNode(g, og *cronsun.Group) {
|
|||
n.groups[g.ID] = g
|
||||
}
|
||||
|
||||
func (n *Node) KillExcutingProc(process *cronsun.Process) {
|
||||
pid, _ := strconv.Atoi(process.ID)
|
||||
if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil {
|
||||
log.Warnf("process:[%d] force kill failed, error:[%s]\n", pid, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) watchJobs() {
|
||||
rch := cronsun.WatchJobs()
|
||||
for wresp := range rch {
|
||||
|
@ -452,6 +461,35 @@ func (n *Node) watchJobs() {
|
|||
}
|
||||
}
|
||||
|
||||
func (n *Node) watchExcutingProc() {
|
||||
rch := cronsun.WatchProcs(n.ID)
|
||||
|
||||
for wresp := range rch {
|
||||
for _, ev := range wresp.Events {
|
||||
switch {
|
||||
case ev.IsModify():
|
||||
key := string(ev.Kv.Key)
|
||||
process, err := cronsun.GetProcFromKey(key)
|
||||
if err != nil {
|
||||
log.Warnf("err: %s, kv: %s", err.Error(), ev.Kv.String())
|
||||
continue
|
||||
}
|
||||
|
||||
val := string(ev.Kv.Value)
|
||||
pv := &cronsun.ProcessVal{}
|
||||
err = json.Unmarshal([]byte(val), pv)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
process.ProcessVal = *pv
|
||||
if process.Killed {
|
||||
n.KillExcutingProc(process)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) watchGroups() {
|
||||
rch := cronsun.WatchGroups()
|
||||
for wresp := range rch {
|
||||
|
@ -531,6 +569,7 @@ func (n *Node) Run() (err error) {
|
|||
|
||||
n.Cron.Start()
|
||||
go n.watchJobs()
|
||||
go n.watchExcutingProc()
|
||||
go n.watchGroups()
|
||||
go n.watchOnce()
|
||||
go n.watchCsctl()
|
||||
|
|
|
@ -186,9 +186,10 @@ func monitorNodes(n Noticer) {
|
|||
|
||||
if node.Alived {
|
||||
n.Send(&Message{
|
||||
Subject: fmt.Sprintf("Node[%s] break away cluster at %s", node.Hostname, time.Now().Format(time.RFC3339)),
|
||||
Body: fmt.Sprintf("Node breaked away cluster, this might happened when node crash or network problems.\nUUID: %s\nHostname: %s\nIP: %s\n", id, node.Hostname, node.IP),
|
||||
To: conf.Config.Mail.To,
|
||||
Subject: fmt.Sprintf("[Cronsun Warning] Node[%s] break away cluster at %s",
|
||||
node.Hostname, time.Now().Format(time.RFC3339)),
|
||||
Body: fmt.Sprintf("Cronsun Node breaked away cluster, this might happened when node crash or network problems.\nUUID: %s\nHostname: %s\nIP: %s\n", id, node.Hostname, node.IP),
|
||||
To: conf.Config.Mail.To,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
41
proc.go
41
proc.go
|
@ -1,6 +1,7 @@
|
|||
package cronsun
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -127,11 +128,13 @@ func (l *leaseID) keepAlive() {
|
|||
// value: 开始执行时间
|
||||
// key 会自动过期,防止进程意外退出后没有清除相关 key,过期时间可配置
|
||||
type Process struct {
|
||||
ID string `json:"id"` // pid
|
||||
JobID string `json:"jobId"`
|
||||
Group string `json:"group"`
|
||||
NodeID string `json:"nodeId"`
|
||||
Time time.Time `json:"time"` // 开始执行时间
|
||||
// parse from key path
|
||||
ID string `json:"id"` // pid
|
||||
JobID string `json:"jobId"`
|
||||
Group string `json:"group"`
|
||||
NodeID string `json:"nodeId"`
|
||||
// parse from value
|
||||
ProcessVal
|
||||
|
||||
running int32
|
||||
hasPut int32
|
||||
|
@ -139,6 +142,11 @@ type Process struct {
|
|||
done chan struct{}
|
||||
}
|
||||
|
||||
type ProcessVal struct {
|
||||
Time time.Time `json:"time"` // 开始执行时间
|
||||
Killed bool `json:"killed"` // 是否强制杀死
|
||||
}
|
||||
|
||||
func GetProcFromKey(key string) (proc *Process, err error) {
|
||||
ss := strings.Split(key, "/")
|
||||
var sslen = len(ss)
|
||||
|
@ -160,11 +168,16 @@ func (p *Process) Key() string {
|
|||
return conf.Config.Proc + p.NodeID + "/" + p.Group + "/" + p.JobID + "/" + p.ID
|
||||
}
|
||||
|
||||
func (p *Process) Val() string {
|
||||
return p.Time.Format(time.RFC3339)
|
||||
func (p *Process) Val() (string, error) {
|
||||
b, err := json.Marshal(&p.ProcessVal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// 获取结点正在执行任务的数量
|
||||
// 获取节点正在执行任务的数量
|
||||
func (j *Job) CountRunning() (int64, error) {
|
||||
resp, err := DefalutClient.Get(conf.Config.Proc+j.runOn+"/"+j.Group+"/"+j.ID, client.WithPrefix(), client.WithCountOnly())
|
||||
if err != nil {
|
||||
|
@ -187,13 +200,17 @@ func (p *Process) put() (err error) {
|
|||
}
|
||||
|
||||
id := lID.get()
|
||||
val, err := p.Val()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id < 0 {
|
||||
if _, err = DefalutClient.Put(p.Key(), p.Val()); err != nil {
|
||||
if _, err = DefalutClient.Put(p.Key(), val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = DefalutClient.Put(p.Key(), p.Val(), client.WithLease(id))
|
||||
_, err = DefalutClient.Put(p.Key(), val, client.WithLease(id))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -254,3 +271,7 @@ func (p *Process) Stop() {
|
|||
log.Warnf("proc del[%s] err: %s", p.Key(), err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func WatchProcs(nid string) client.WatchChan {
|
||||
return DefalutClient.Watch(conf.Config.Proc+nid, client.WithPrefix())
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"runtime"
|
||||
)
|
||||
|
||||
const VersionNumber = "0.3.2"
|
||||
const VersionNumber = "0.3.4"
|
||||
|
||||
var (
|
||||
Version = fmt.Sprintf("v%s (build %s)", VersionNumber, runtime.Version())
|
||||
|
|
91
web/job.go
91
web/job.go
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -220,6 +219,7 @@ func (j *Job) GetList(ctx *Context) {
|
|||
type jobStatus struct {
|
||||
*cronsun.Job
|
||||
LatestStatus *cronsun.JobLatestLog `json:"latestStatus"`
|
||||
NextRunTime string `json:"nextRunTime"`
|
||||
}
|
||||
|
||||
resp, err := cronsun.DefalutClient.Get(prefix, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
|
||||
|
@ -264,6 +264,12 @@ func (j *Job) GetList(ctx *Context) {
|
|||
} else {
|
||||
for i := range jobList {
|
||||
jobList[i].LatestStatus = m[jobList[i].ID]
|
||||
nt := jobList[i].GetNextRunTime()
|
||||
if nt.IsZero() {
|
||||
jobList[i].NextRunTime = "NO!!"
|
||||
} else {
|
||||
jobList[i].NextRunTime = nt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,7 +345,7 @@ func (j *Job) GetExecutingJob(ctx *Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var list = make([]*cronsun.Process, 0, 8)
|
||||
var list = make([]*processInfo, 0, 8)
|
||||
for i := range gresp.Kvs {
|
||||
proc, err := cronsun.GetProcFromKey(string(gresp.Kvs[i].Key))
|
||||
if err != nil {
|
||||
|
@ -350,14 +356,84 @@ func (j *Job) GetExecutingJob(ctx *Context) {
|
|||
if !opt.Match(proc) {
|
||||
continue
|
||||
}
|
||||
proc.Time, _ = time.Parse(time.RFC3339, string(gresp.Kvs[i].Value))
|
||||
list = append(list, proc)
|
||||
|
||||
val := string(gresp.Kvs[i].Value)
|
||||
var pv = &cronsun.ProcessVal{}
|
||||
err = json.Unmarshal([]byte(val), pv)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to unmarshal ProcessVal from val: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
proc.ProcessVal = *pv
|
||||
procInfo := &processInfo{
|
||||
Process: proc,
|
||||
}
|
||||
job, err := cronsun.GetJob(proc.Group, proc.JobID)
|
||||
if err == nil && job != nil {
|
||||
procInfo.JobName = job.Name
|
||||
} else {
|
||||
procInfo.JobName = proc.JobID
|
||||
}
|
||||
list = append(list, procInfo)
|
||||
}
|
||||
|
||||
sort.Sort(ByProcTime(list))
|
||||
outJSON(ctx.W, list)
|
||||
}
|
||||
|
||||
func (j *Job) KillExecutingJob(ctx *Context) {
|
||||
proc := &cronsun.Process{
|
||||
ID: getStringVal("pid", ctx.R),
|
||||
JobID: getStringVal("job", ctx.R),
|
||||
Group: getStringVal("group", ctx.R),
|
||||
NodeID: getStringVal("node", ctx.R),
|
||||
}
|
||||
|
||||
if proc.ID == "" || proc.JobID == "" || proc.Group == "" || proc.NodeID == "" {
|
||||
outJSONWithCode(ctx.W, http.StatusBadRequest, "Invalid process info.")
|
||||
return
|
||||
}
|
||||
|
||||
procKey := proc.Key()
|
||||
resp, err := cronsun.DefalutClient.Get(procKey)
|
||||
if err != nil {
|
||||
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Kvs) < 1 {
|
||||
outJSONWithCode(ctx.W, http.StatusNotFound, "Porcess not found")
|
||||
return
|
||||
}
|
||||
|
||||
var procVal = &cronsun.ProcessVal{}
|
||||
err = json.Unmarshal(resp.Kvs[0].Value, &procVal)
|
||||
if err != nil {
|
||||
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if procVal.Killed {
|
||||
outJSONWithCode(ctx.W, http.StatusOK, "Killing process")
|
||||
return
|
||||
}
|
||||
|
||||
procVal.Killed = true
|
||||
proc.ProcessVal = *procVal
|
||||
str, err := proc.Val()
|
||||
if err != nil {
|
||||
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cronsun.DefalutClient.Put(procKey, str)
|
||||
if err != nil {
|
||||
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
outJSONWithCode(ctx.W, http.StatusOK, "Killing process")
|
||||
}
|
||||
|
||||
type ProcFetchOptions struct {
|
||||
Groups []string
|
||||
NodeIds []string
|
||||
|
@ -381,7 +457,12 @@ func (opt *ProcFetchOptions) Match(proc *cronsun.Process) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
type ByProcTime []*cronsun.Process
|
||||
type processInfo struct {
|
||||
*cronsun.Process
|
||||
JobName string `json:"jobName"`
|
||||
}
|
||||
|
||||
type ByProcTime []*processInfo
|
||||
|
||||
func (a ByProcTime) Len() int { return len(a) }
|
||||
func (a ByProcTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
|
|
@ -76,6 +76,10 @@ func initRouters() (s *http.Server, err error) {
|
|||
h = NewAuthHandler(jobHandler.GetExecutingJob, cronsun.Reporter)
|
||||
subrouter.Handle("/job/executing", h).Methods("GET")
|
||||
|
||||
// kill an executing job
|
||||
h = NewAuthHandler(jobHandler.KillExecutingJob, cronsun.Developer)
|
||||
subrouter.Handle("/job/executing", h).Methods("DELETE")
|
||||
|
||||
// get job log list
|
||||
h = NewAuthHandler(jobLogHandler.GetList, cronsun.Reporter)
|
||||
subrouter.Handle("/logs", h).Methods("GET")
|
||||
|
@ -83,7 +87,7 @@ func initRouters() (s *http.Server, err error) {
|
|||
h = NewAuthHandler(jobLogHandler.GetDetail, cronsun.Developer)
|
||||
subrouter.Handle("/log/{id}", h).Methods("GET")
|
||||
|
||||
h = NewAuthHandler(nodeHandler.GetNodes, cronsun.Developer)
|
||||
h = NewAuthHandler(nodeHandler.GetNodes, cronsun.Reporter)
|
||||
subrouter.Handle("/nodes", h).Methods("GET")
|
||||
h = NewAuthHandler(nodeHandler.DeleteNode, cronsun.Developer)
|
||||
subrouter.Handle("/node/{ip}", h).Methods("DELETE")
|
||||
|
@ -144,8 +148,8 @@ func (s *embeddedFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
fp += s.IndexFile
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Expires", "0")
|
||||
// w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
// w.Header().Set("Expires", "0")
|
||||
|
||||
b, err = Asset(fp)
|
||||
if err == nil {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -61,7 +61,7 @@
|
|||
<div id="app">
|
||||
<div id="initloader"></div>
|
||||
</div>
|
||||
<script src="build.js?v=6fe9c2f"></script>
|
||||
<script src="build.js?v=1d90ef8"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --inline --hot",
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --inline --hot --disableHostCheck=true",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules && cp index.html ./dist/",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
@ -13,7 +13,7 @@
|
|||
"chart.js": "^2.5.0",
|
||||
"jquery": "^3.1.1",
|
||||
"jquery.cookie": "^1.4.1",
|
||||
"semantic-ui": "^2.2.7",
|
||||
"semantic-ui": "^2.3.3",
|
||||
"vue": "^2.3.4",
|
||||
"vue-router": "^2.2.1",
|
||||
"vuex": "^2.3.1"
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<tr>
|
||||
<th class="collapsing center aligned">{{$L('operation')}}</th>
|
||||
<th class="collapsing center aligned">{{$L('status')}}</th>
|
||||
<th width="200px" class="center aligned">{{$L('group')}}</th>
|
||||
<th class="center aligned">{{$L('group')}}</th>
|
||||
<th class="center aligned">{{$L('user')}}</th>
|
||||
<th class="center aligned">{{$L('name')}}</th>
|
||||
<th class="center aligned">{{$L('latest executed')}}</th>
|
||||
|
@ -66,6 +66,8 @@
|
|||
<td>
|
||||
<span v-if="!job.latestStatus">-</span>
|
||||
<span v-else>{{formatLatest(job.latestStatus)}}</span>
|
||||
<br/>
|
||||
<span>{{formatNextRunTime(job.nextRunTime)}}</span>
|
||||
</td>
|
||||
<td :class="{error: job.latestStatus && !job.latestStatus.success}">
|
||||
<span v-if="!job.latestStatus">-</span>
|
||||
|
@ -186,6 +188,10 @@ export default {
|
|||
return this.$L('on {node} took {times}, {begin ~ end}', this.$store.getters.hostshowsWithoutTip(latest.node), formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
|
||||
},
|
||||
|
||||
formatNextRunTime: function(nextRunTime){
|
||||
return this.$L('next schedule: {nextTime}', nextRunTime);
|
||||
},
|
||||
|
||||
showExecuteJobModal: function(jobName, jobGroup, jobId){
|
||||
this.$refs.executeJobModal.show(jobName, jobGroup, jobId);
|
||||
},
|
||||
|
|
|
@ -211,7 +211,7 @@ export default {
|
|||
}
|
||||
}
|
||||
}).
|
||||
onfailed((msg)=> vm.$bus.$emit('error', data)).
|
||||
onfailed((msg)=> vm.$bus.$emit('error', msg)).
|
||||
do();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="field">
|
||||
<div class="ui icon input">
|
||||
<input type="text" v-bind:value="rule.timer" v-on:input="change('timer', $event.target.value)" :placeholder="$L('0 * * * * *, rules see the 「?」on the right')"/>
|
||||
<i ref="ruletip" class="large help circle link icon" data-position="top right" :data-content="$L('<sec> <min> <hr> <day> <month> <week>, rules is same with Cron')" data-variation="wide"></i>
|
||||
<i ref="ruletip" class="large help circle link icon" data-position="top right" :data-html="$L('<sec> <min> <hr> <day> <month> <week>, rules is same with Cron')" data-variation="wide"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<style scope>
|
||||
.clearfix:after {content:""; clear:both; display:table;}
|
||||
.kill-proc-btn { color:red;cursor: pointer;}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
|
@ -28,20 +29,22 @@
|
|||
<table class="ui hover blue table" v-if="executings.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="center aligned">{{$L('job ID')}}</th>
|
||||
<th class="center aligned">{{$L('job name')}}</th>
|
||||
<th width="200px" class="center aligned">{{$L('job group')}}</th>
|
||||
<th class="center aligned">{{$L('node')}}</th>
|
||||
<th class="center aligned">{{$L('process ID')}}</th>
|
||||
<th class="center aligned">{{$L('starting time')}}</th>
|
||||
<th class="center aligned">{{$L('operation')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(proc, index) in executings">
|
||||
<td class="center aligned"><router-link :to="'/job/edit/'+proc.group+'/'+proc.jobId">{{proc.jobId}}</router-link></td>
|
||||
<td class="center aligned"><router-link :to="'/job/edit/'+proc.group+'/'+proc.jobId">{{proc.jobName}}</router-link></td>
|
||||
<td class="center aligned">{{proc.group}}</td>
|
||||
<td class="center aligned">{{$store.getters.hostshows(proc.nodeId)}}</td>
|
||||
<td class="center aligned">{{proc.id}}</td>
|
||||
<td class="center aligned">{{proc.time}}</td>
|
||||
<td class="center aligned"><a class="kill-proc-btn" v-on:click="killProc(proc, index)">{{$L('kill process')}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -100,6 +103,20 @@ export default {
|
|||
this.$router.push('/job/executing?'+this.buildQuery());
|
||||
},
|
||||
|
||||
killProc(proc, index) {
|
||||
if (!confirm(this.$L('whether to kill the process'))) return;
|
||||
var vm = this
|
||||
var params = []
|
||||
params.push('node='+proc.nodeId)
|
||||
params.push('group='+proc.group)
|
||||
params.push('job='+proc.jobId)
|
||||
params.push('pid='+proc.id)
|
||||
this.$rest.DELETE('job/executing?' + params.join('&'))
|
||||
.onsucceed(200, (resp) => vm.$bus.$emit('success', vm.$L('command has been sent to the node')))
|
||||
.onfailed((resp) => vm.$bus.$emit('error', resp))
|
||||
.do();
|
||||
},
|
||||
|
||||
buildQuery(){
|
||||
var params = [];
|
||||
if (this.groups && this.groups.length > 0) params.push('groups='+this.groups.join(','));
|
||||
|
|
|
@ -89,7 +89,7 @@ export default {
|
|||
begin: '',
|
||||
end: '',
|
||||
latest: false,
|
||||
failedOnly: '',
|
||||
failedOnly: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1
|
||||
|
@ -101,8 +101,8 @@ export default {
|
|||
this.fetchList(this.buildQuery());
|
||||
|
||||
var vm = this;
|
||||
$(this.$refs.latest).checkbox({'onChange': ()=>{vm.latest = !vm.latest}});
|
||||
$(this.$refs.failedOnly).checkbox({'onChange': ()=>{vm.failedOnly = !vm.failedOnly}});
|
||||
$(this.$refs.latest).checkbox();
|
||||
$(this.$refs.failedOnly).checkbox();
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -121,8 +121,8 @@ export default {
|
|||
this.begin = this.$route.query.begin || '';
|
||||
this.end = this.$route.query.end || '';
|
||||
this.page = this.$route.query.page || 1;
|
||||
this.latest = this.$route.query.latest == 'true' ? true : false;
|
||||
this.failedOnly = this.$route.query.failedOnly ? true : false;
|
||||
this.latest = this.$route.query.latest === 'true' || this.$route.query.latest === true;
|
||||
this.failedOnly = this.$route.query.failedOnly === 'true' || this.$route.query.failedOnly === true;
|
||||
},
|
||||
|
||||
fetchList(query){
|
||||
|
|
|
@ -54,6 +54,10 @@ var language = {
|
|||
'view job list': 'View job list',
|
||||
'starting time': 'Starting time',
|
||||
'process ID': 'Process ID',
|
||||
'kill process': 'Kill process',
|
||||
'whether to kill the process': 'Whether to kill the process',
|
||||
'command has been sent to the node': 'Command has been sent to the node',
|
||||
|
||||
'group filter': 'Group filter',
|
||||
'node filter': 'Node filter',
|
||||
'select a group': 'Select a group',
|
||||
|
@ -71,6 +75,7 @@ var language = {
|
|||
'all groups': 'All groups',
|
||||
'all nodes': 'All nodes',
|
||||
'on {node} took {times}, {begin ~ end}': 'On {0} took {1}, {2}',
|
||||
'next schedule: {nextTime}': 'Next schedule: {0}',
|
||||
'create job': 'Create job',
|
||||
'update job': 'Update job',
|
||||
'output': 'Output',
|
||||
|
@ -102,7 +107,10 @@ var language = {
|
|||
'timeout(in seconds, 0 for no limits)': 'Timeout(in seconds, 0 for no limits)',
|
||||
'log expiration(log expired after N days, 0 will use default setting: {n} days)': 'Log expiration(log expired after N days, 0 will use default setting: {0} days)',
|
||||
'0 * * * * *, rules see the 「?」on the right': '0 * * * * *, rules see the 「?」on the right',
|
||||
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<sec> <min> <hr> <day> <month> <week>, rules is same with Cron',
|
||||
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<sec> <min> <hour> <day> <month> <week>, rules is same with Cron.' +
|
||||
'<br/>If want run job once at special time (like Linux\'s "at" command), you can use "@at 2006-01-02 15:04:05" to set it.' +
|
||||
'<br/>You may use one of several pre-defined schedules in place of a cron expression. "@hourly" run once an hour, beginning of hour.' +
|
||||
'<br/>More detail please visit the wiki.',
|
||||
'and please running on those nodes': 'And please running on those nodes',
|
||||
'do not running on those nodes': 'Do not running on those nodes',
|
||||
'the job dose not have a timer currently, please click the button below to add a timer': 'The job dose not have a timer currently, please click the button below to add a timer',
|
||||
|
|
|
@ -55,6 +55,9 @@ var language = {
|
|||
'view job list': '查看任务列表',
|
||||
'starting time': '开始时间',
|
||||
'process ID': '进程ID',
|
||||
'kill process': '杀死进程',
|
||||
'whether to kill the process': '是否杀死该进程',
|
||||
'command has been sent to the node': '命令已经发送到节点',
|
||||
|
||||
'group filter': '分组过滤',
|
||||
'node filter': '节点过滤',
|
||||
|
@ -73,6 +76,7 @@ var language = {
|
|||
'all groups': '所有分组',
|
||||
'all nodes': '所有节点',
|
||||
'on {node} took {times}, {begin ~ end}': '于 {0} 耗时 {1}, {2}',
|
||||
'next schedule: {nextTime}': '下个调度: {0}',
|
||||
'create job': '新建任务',
|
||||
'update job': '更新任务',
|
||||
'output': '输出',
|
||||
|
@ -87,8 +91,8 @@ var language = {
|
|||
'single node single process': '单机单进程',
|
||||
'group level common': '组级别普通任务',
|
||||
'group level common help': '暂时没想到好名字,一个比较简单的说明是,把所有选中的节点视为一个大节点,那么该类型的任务就相当于在单个节点上的普通任务',
|
||||
'warning on': '开启报警',
|
||||
'warning off': '关闭报警',
|
||||
'warning on': '报警已开启',
|
||||
'warning off': '报警已关闭',
|
||||
'job group': '任务分组',
|
||||
'script path': '任务脚本',
|
||||
'(only [{.suffixs}] files can be allowed)': '(只允许 [{0}] 文件)',
|
||||
|
@ -104,7 +108,10 @@ var language = {
|
|||
'timeout(in seconds, 0 for no limits)': '超时设置(单位“秒”,0 表示不限制)',
|
||||
'log expiration(log expired after N days, 0 will use default setting: {n} days)': '日志过期(日志保存天数,0 表示使用默认设置:{0} 天)',
|
||||
'0 * * * * *, rules see the 「?」on the right': '0 * * * * *, 规则参考右边的「?」',
|
||||
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<秒> <分> <时> <日> <月> <周>,规则与 Cron 一样',
|
||||
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<秒> <分> <时> <日> <月> <周>,规则与 Cron 一样。' +
|
||||
'<br/>如果要指定只在某个时间点执行一次(类似Linux系统的at命令),可以使用 "@at 2006-01-02 15:04:05" 这样来设定。' +
|
||||
'<br/>也支持一些简写,例如 @daily 表示每天执行一次。' +
|
||||
'<br/>更多请参考wiki。',
|
||||
'and please running on those nodes': '同时在这些节点上面运行',
|
||||
'do not running on those nodes': '不要在这些节点上面运行',
|
||||
'the job dose not have a timer currently, please click the button below to add a timer': '当前任务没有定时器,点击下面按钮来添加定时器',
|
||||
|
|
|
@ -4,9 +4,27 @@ require('semantic-ui/dist/semantic.min.css');
|
|||
import store from './vuex/store';
|
||||
|
||||
import Vue from 'vue';
|
||||
import Lang from './i18n/language';
|
||||
// global restful client
|
||||
import Rest from './libraries/rest-client.js';
|
||||
import VueRouter from 'vue-router';
|
||||
import App from './App.vue';
|
||||
import Dash from './components/Dash.vue';
|
||||
import Log from './components/Log.vue';
|
||||
import LogDetail from './components/LogDetail.vue';
|
||||
import Job from './components/Job.vue';
|
||||
import JobEdit from './components/JobEdit.vue';
|
||||
import JobExecuting from './components/JobExecuting.vue';
|
||||
import Node from './components/Node.vue';
|
||||
import NodeGroup from './components/NodeGroup.vue';
|
||||
import NodeGroupEdit from './components/NodeGroupEdit.vue';
|
||||
import Account from './components/Account.vue';
|
||||
import AccountEdit from './components/AccountEdit.vue';
|
||||
import Profile from './components/Profile.vue';
|
||||
import Login from './components/Login.vue';
|
||||
|
||||
Vue.config.debug = true;
|
||||
|
||||
import Lang from './i18n/language';
|
||||
Vue.use((Vue) => {
|
||||
Vue.prototype.$L = Lang.L
|
||||
Vue.prototype.$Lang = Lang
|
||||
|
@ -18,35 +36,34 @@ Vue.use((Vue) => {
|
|||
Vue.prototype.$bus = bus;
|
||||
});
|
||||
|
||||
// global restful client
|
||||
import Rest from './libraries/rest-client.js';
|
||||
|
||||
var restApi = new Rest('/v1/', (msg) => {
|
||||
bus.$emit('error', msg);
|
||||
}, (msg) => {
|
||||
bus.$emit('error', msg);
|
||||
}, {
|
||||
401: (data, xhr) => { bus.$emit('goLogin') }
|
||||
});
|
||||
401: (data, xhr) => {
|
||||
bus.$emit('goLogin')
|
||||
}
|
||||
});
|
||||
Vue.use((Vue, options) => {
|
||||
Vue.prototype.$rest = restApi;
|
||||
}, null);
|
||||
|
||||
import VueRouter from 'vue-router';
|
||||
Vue.use(VueRouter);
|
||||
|
||||
Vue.use((Vue) => {
|
||||
Vue.prototype.$loadConfiguration = () => {
|
||||
restApi.GET('configurations').
|
||||
onsucceed(200, (resp) => {
|
||||
const Config = (Vue, options) => {
|
||||
Vue.prototype.$appConfig = resp;
|
||||
}
|
||||
Vue.use(Config);
|
||||
bus.$emit('conf_loaded', resp);
|
||||
}).onfailed((data, xhr) => {
|
||||
var msg = data ? data : xhr.status + ' ' + xhr.statusText;
|
||||
bus.$emit('error', msg);
|
||||
}).do();
|
||||
restApi.GET('configurations').onsucceed(200, (resp) => {
|
||||
const Config = (Vue, options) => {
|
||||
Vue.prototype.$appConfig = resp;
|
||||
}
|
||||
Vue.use(Config);
|
||||
bus.$emit('conf_loaded', resp);
|
||||
}).onfailed((data, xhr) => {
|
||||
var msg = data ? data : xhr.status + ' ' + xhr.statusText;
|
||||
bus.$emit('error', msg);
|
||||
}).do();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -73,38 +90,23 @@ const onConfigLoaded = (Vue, options) => {
|
|||
}
|
||||
Vue.use(onConfigLoaded);
|
||||
|
||||
import App from './App.vue';
|
||||
import Dash from './components/Dash.vue';
|
||||
import Log from './components/Log.vue';
|
||||
import LogDetail from './components/LogDetail.vue';
|
||||
import Job from './components/Job.vue';
|
||||
import JobEdit from './components/JobEdit.vue';
|
||||
import JobExecuting from './components/JobExecuting.vue';
|
||||
import Node from './components/Node.vue';
|
||||
import NodeGroup from './components/NodeGroup.vue';
|
||||
import NodeGroupEdit from './components/NodeGroupEdit.vue';
|
||||
import Account from './components/Account.vue';
|
||||
import AccountEdit from './components/AccountEdit.vue';
|
||||
import Profile from './components/Profile.vue';
|
||||
import Login from './components/Login.vue';
|
||||
|
||||
var routes = [
|
||||
{ path: '/', component: Dash },
|
||||
{ path: '/log', component: Log },
|
||||
{ path: '/log/:id', component: LogDetail },
|
||||
{ path: '/job', component: Job },
|
||||
{ path: '/job/create', component: JobEdit },
|
||||
{ path: '/job/edit/:group/:id', component: JobEdit },
|
||||
{ path: '/job/executing', component: JobExecuting },
|
||||
{ path: '/node', component: Node },
|
||||
{ path: '/node/group', component: NodeGroup },
|
||||
{ path: '/node/group/create', component: NodeGroupEdit },
|
||||
{ path: '/node/group/:id', component: NodeGroupEdit },
|
||||
{ path: '/admin/account/list', component: Account },
|
||||
{ path: '/admin/account/add', component: AccountEdit },
|
||||
{ path: '/admin/account/edit', component: AccountEdit },
|
||||
{ path: '/user/setpwd', component: Profile },
|
||||
{ path: '/login', component: Login }
|
||||
{path: '/', component: Dash},
|
||||
{path: '/log', component: Log},
|
||||
{path: '/log/:id', component: LogDetail},
|
||||
{path: '/job', component: Job},
|
||||
{path: '/job/create', component: JobEdit},
|
||||
{path: '/job/edit/:group/:id', component: JobEdit},
|
||||
{path: '/job/executing', component: JobExecuting},
|
||||
{path: '/node', component: Node},
|
||||
{path: '/node/group', component: NodeGroup},
|
||||
{path: '/node/group/create', component: NodeGroupEdit},
|
||||
{path: '/node/group/:id', component: NodeGroupEdit},
|
||||
{path: '/admin/account/list', component: Account},
|
||||
{path: '/admin/account/add', component: AccountEdit},
|
||||
{path: '/admin/account/edit', component: AccountEdit},
|
||||
{path: '/user/setpwd', component: Profile},
|
||||
{path: '/login', component: Login}
|
||||
];
|
||||
|
||||
var router = new VueRouter({
|
||||
|
@ -118,29 +120,32 @@ bus.$on('goLogin', () => {
|
|||
});
|
||||
|
||||
var initConf = new Promise((resolve) => {
|
||||
restApi.GET('session?check=1').
|
||||
onsucceed(200, (resp) => {
|
||||
restApi.GET('session?check=1').onsucceed(200, (resp) => {
|
||||
store.commit('enabledAuth', resp.enabledAuth);
|
||||
store.commit('setEmail', resp.email);
|
||||
store.commit('setRole', resp.role);
|
||||
|
||||
restApi.GET('version').onsucceed(200, (resp)=>{
|
||||
restApi.GET('version').onsucceed(200, (resp) => {
|
||||
store.commit('setVersion', resp);
|
||||
}).do();
|
||||
|
||||
restApi.GET('configurations').
|
||||
onsucceed(200, (resp) => {
|
||||
|
||||
restApi.GET('configurations').onsucceed(200, (resp) => {
|
||||
Vue.use((Vue) => Vue.prototype.$appConfig = resp);
|
||||
bus.$emit('conf_loaded', resp);
|
||||
|
||||
restApi.GET('nodes').onsucceed(200, (resp)=>{
|
||||
var nodes = {};
|
||||
for (var i in resp) {
|
||||
nodes[resp[i].id] = resp[i];
|
||||
}
|
||||
store.commit('setNodes', nodes);
|
||||
resolve();
|
||||
}).do();
|
||||
var loadNodes = function() {
|
||||
restApi.GET('nodes').onsucceed(200, (resp) => {
|
||||
var nodes = {};
|
||||
for (var i in resp) {
|
||||
nodes[resp[i].id] = resp[i];
|
||||
}
|
||||
store.commit('setNodes', nodes);
|
||||
resolve();
|
||||
}).do();
|
||||
}
|
||||
loadNodes();
|
||||
setInterval(loadNodes, 60*1000);
|
||||
|
||||
}).onfailed((data, xhr) => {
|
||||
bus.$emit('error', data ? data : xhr.status + ' ' + xhr.statusText);
|
||||
resolve();
|
||||
|
@ -153,8 +158,7 @@ var initConf = new Promise((resolve) => {
|
|||
}
|
||||
router.push('/login');
|
||||
resolve()
|
||||
}).
|
||||
do();
|
||||
}).do();
|
||||
})
|
||||
|
||||
initConf.then(() => {
|
||||
|
|
Loading…
Reference in New Issue