导入任务,并增加ls命令,可以列出当前node

pull/113/head
xyb 2018-10-18 23:44:35 +08:00
commit a18f5ac3fc
33 changed files with 1075 additions and 233 deletions

View File

@ -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 ./...

View File

@ -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

View File

@ -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` 进行登录
### 关于后台权限

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -3,6 +3,8 @@
"127.0.0.1:27017"
],
"Database": "cronsun",
"UserName": "",
"Password": "",
"#Timeout": "connect timeout duration/second",
"Timeout": 15
}

View File

@ -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",

View File

@ -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 '/'.")

60
go.mod Normal file
View File

@ -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
)

112
go.sum Normal file
View File

@ -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
View File

@ -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
}

34
node/cron/at.go Normal file
View File

@ -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{}
}

89
node/cron/at_test.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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
View File

@ -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())
}

View File

@ -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())

View File

@ -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] }

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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);
},

View File

@ -211,7 +211,7 @@ export default {
}
}
}).
onfailed((msg)=> vm.$bus.$emit('error', data)).
onfailed((msg)=> vm.$bus.$emit('error', msg)).
do();
}

View File

@ -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">

View File

@ -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(','));

View File

@ -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){

View File

@ -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': '&lt;sec&gt; &lt;min&gt; &lt;hour&gt; &lt;day&gt; &lt;month&gt; &lt;week&gt;, 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',

View File

@ -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': '当前任务没有定时器,点击下面按钮来添加定时器',

View File

@ -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(() => {