mirror of https://github.com/shunfei/cronsun
commit
f7de1efe85
21
job.go
21
job.go
|
@ -411,6 +411,26 @@ func (j *Job) String() string {
|
||||||
return string(data)
|
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 执行任务
|
// Run 执行任务
|
||||||
func (j *Job) Run() bool {
|
func (j *Job) Run() bool {
|
||||||
var (
|
var (
|
||||||
|
@ -738,6 +758,5 @@ func (j *Job) CreateCmdAttr() (*syscall.SysProcAttr, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return sysProcAttr, nil
|
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
|
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)
|
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,6 +219,7 @@ func (j *Job) GetList(ctx *Context) {
|
||||||
type jobStatus struct {
|
type jobStatus struct {
|
||||||
*cronsun.Job
|
*cronsun.Job
|
||||||
LatestStatus *cronsun.JobLatestLog `json:"latestStatus"`
|
LatestStatus *cronsun.JobLatestLog `json:"latestStatus"`
|
||||||
|
NextRunTime string `json:"nextRunTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := cronsun.DefalutClient.Get(prefix, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
|
resp, err := cronsun.DefalutClient.Get(prefix, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
|
||||||
|
@ -263,6 +264,12 @@ func (j *Job) GetList(ctx *Context) {
|
||||||
} else {
|
} else {
|
||||||
for i := range jobList {
|
for i := range jobList {
|
||||||
jobList[i].LatestStatus = m[jobList[i].ID]
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,8 +148,8 @@ func (s *embeddedFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
fp += s.IndexFile
|
fp += s.IndexFile
|
||||||
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
// w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
w.Header().Set("Expires", "0")
|
// w.Header().Set("Expires", "0")
|
||||||
|
|
||||||
b, err = Asset(fp)
|
b, err = Asset(fp)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -61,7 +61,7 @@
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="initloader"></div>
|
<div id="initloader"></div>
|
||||||
</div>
|
</div>
|
||||||
<script src="build.js?v=8fbb71c"></script>
|
<script src="build.js?v=1d90ef8"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="collapsing center aligned">{{$L('operation')}}</th>
|
<th class="collapsing center aligned">{{$L('operation')}}</th>
|
||||||
<th class="collapsing center aligned">{{$L('status')}}</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('user')}}</th>
|
||||||
<th class="center aligned">{{$L('name')}}</th>
|
<th class="center aligned">{{$L('name')}}</th>
|
||||||
<th class="center aligned">{{$L('latest executed')}}</th>
|
<th class="center aligned">{{$L('latest executed')}}</th>
|
||||||
|
@ -66,6 +66,8 @@
|
||||||
<td>
|
<td>
|
||||||
<span v-if="!job.latestStatus">-</span>
|
<span v-if="!job.latestStatus">-</span>
|
||||||
<span v-else>{{formatLatest(job.latestStatus)}}</span>
|
<span v-else>{{formatLatest(job.latestStatus)}}</span>
|
||||||
|
<br/>
|
||||||
|
<span>{{formatNextRunTime(job.nextRunTime)}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td :class="{error: job.latestStatus && !job.latestStatus.success}">
|
<td :class="{error: job.latestStatus && !job.latestStatus.success}">
|
||||||
<span v-if="!job.latestStatus">-</span>
|
<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));
|
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){
|
showExecuteJobModal: function(jobName, jobGroup, jobId){
|
||||||
this.$refs.executeJobModal.show(jobName, jobGroup, jobId);
|
this.$refs.executeJobModal.show(jobName, jobGroup, jobId);
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui icon input">
|
<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')"/>
|
<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>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
@ -89,7 +89,7 @@ export default {
|
||||||
begin: '',
|
begin: '',
|
||||||
end: '',
|
end: '',
|
||||||
latest: false,
|
latest: false,
|
||||||
failedOnly: '',
|
failedOnly: false,
|
||||||
list: [],
|
list: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
page: 1
|
page: 1
|
||||||
|
@ -101,8 +101,8 @@ export default {
|
||||||
this.fetchList(this.buildQuery());
|
this.fetchList(this.buildQuery());
|
||||||
|
|
||||||
var vm = this;
|
var vm = this;
|
||||||
$(this.$refs.latest).checkbox({'onChange': ()=>{vm.latest = !vm.latest}});
|
$(this.$refs.latest).checkbox();
|
||||||
$(this.$refs.failedOnly).checkbox({'onChange': ()=>{vm.failedOnly = !vm.failedOnly}});
|
$(this.$refs.failedOnly).checkbox();
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -121,8 +121,8 @@ export default {
|
||||||
this.begin = this.$route.query.begin || '';
|
this.begin = this.$route.query.begin || '';
|
||||||
this.end = this.$route.query.end || '';
|
this.end = this.$route.query.end || '';
|
||||||
this.page = this.$route.query.page || 1;
|
this.page = this.$route.query.page || 1;
|
||||||
this.latest = this.$route.query.latest == 'true' ? true : false;
|
this.latest = this.$route.query.latest === 'true' || this.$route.query.latest === true;
|
||||||
this.failedOnly = this.$route.query.failedOnly ? true : false;
|
this.failedOnly = this.$route.query.failedOnly === 'true' || this.$route.query.failedOnly === true;
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchList(query){
|
fetchList(query){
|
||||||
|
|
|
@ -75,6 +75,7 @@ var language = {
|
||||||
'all groups': 'All groups',
|
'all groups': 'All groups',
|
||||||
'all nodes': 'All nodes',
|
'all nodes': 'All nodes',
|
||||||
'on {node} took {times}, {begin ~ end}': 'On {0} took {1}, {2}',
|
'on {node} took {times}, {begin ~ end}': 'On {0} took {1}, {2}',
|
||||||
|
'next schedule: {nextTime}': 'Next schedule: {0}',
|
||||||
'create job': 'Create job',
|
'create job': 'Create job',
|
||||||
'update job': 'Update job',
|
'update job': 'Update job',
|
||||||
'output': 'Output',
|
'output': 'Output',
|
||||||
|
@ -106,7 +107,10 @@ var language = {
|
||||||
'timeout(in seconds, 0 for no limits)': 'Timeout(in seconds, 0 for no limits)',
|
'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)',
|
'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',
|
'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',
|
'and please running on those nodes': 'And please running on those nodes',
|
||||||
'do not running on those nodes': 'Do not 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',
|
'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',
|
||||||
|
|
|
@ -76,6 +76,7 @@ var language = {
|
||||||
'all groups': '所有分组',
|
'all groups': '所有分组',
|
||||||
'all nodes': '所有节点',
|
'all nodes': '所有节点',
|
||||||
'on {node} took {times}, {begin ~ end}': '于 {0} 耗时 {1}, {2}',
|
'on {node} took {times}, {begin ~ end}': '于 {0} 耗时 {1}, {2}',
|
||||||
|
'next schedule: {nextTime}': '下个调度: {0}',
|
||||||
'create job': '新建任务',
|
'create job': '新建任务',
|
||||||
'update job': '更新任务',
|
'update job': '更新任务',
|
||||||
'output': '输出',
|
'output': '输出',
|
||||||
|
@ -90,8 +91,8 @@ var language = {
|
||||||
'single node single process': '单机单进程',
|
'single node single process': '单机单进程',
|
||||||
'group level common': '组级别普通任务',
|
'group level common': '组级别普通任务',
|
||||||
'group level common help': '暂时没想到好名字,一个比较简单的说明是,把所有选中的节点视为一个大节点,那么该类型的任务就相当于在单个节点上的普通任务',
|
'group level common help': '暂时没想到好名字,一个比较简单的说明是,把所有选中的节点视为一个大节点,那么该类型的任务就相当于在单个节点上的普通任务',
|
||||||
'warning on': '开启报警',
|
'warning on': '报警已开启',
|
||||||
'warning off': '关闭报警',
|
'warning off': '报警已关闭',
|
||||||
'job group': '任务分组',
|
'job group': '任务分组',
|
||||||
'script path': '任务脚本',
|
'script path': '任务脚本',
|
||||||
'(only [{.suffixs}] files can be allowed)': '(只允许 [{0}] 文件)',
|
'(only [{.suffixs}] files can be allowed)': '(只允许 [{0}] 文件)',
|
||||||
|
@ -107,7 +108,10 @@ var language = {
|
||||||
'timeout(in seconds, 0 for no limits)': '超时设置(单位“秒”,0 表示不限制)',
|
'timeout(in seconds, 0 for no limits)': '超时设置(单位“秒”,0 表示不限制)',
|
||||||
'log expiration(log expired after N days, 0 will use default setting: {n} days)': '日志过期(日志保存天数,0 表示使用默认设置:{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 * * * * *, 规则参考右边的「?」',
|
'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': '同时在这些节点上面运行',
|
'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': '当前任务没有定时器,点击下面按钮来添加定时器',
|
||||||
|
|
Loading…
Reference in New Issue