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)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
|
@ -738,6 +758,5 @@ func (j *Job) CreateCmdAttr() (*syscall.SysProcAttr, error) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -219,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))
|
||||
|
@ -263,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -148,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=8fbb71c"></script>
|
||||
<script src="build.js?v=1d90ef8"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -75,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',
|
||||
|
@ -106,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',
|
||||
|
|
|
@ -76,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': '输出',
|
||||
|
@ -90,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}] 文件)',
|
||||
|
@ -107,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': '当前任务没有定时器,点击下面按钮来添加定时器',
|
||||
|
|
Loading…
Reference in New Issue