diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ba44eba --- /dev/null +++ b/TODO.md @@ -0,0 +1,23 @@ +# Frontend + +1. UI to view + 1. running job + 1. jobs + 1. job definition + 1. next runtime + 1. last runtime + 1. last output +1. UI to mutate + 1. submit job + 1. delete job + 1. pause jobs + 1. interrupt job +1. JS + 1. ajax for json calls + +# Backend + +1. load from file +1. interrupt running jobs +1. temporarily disable jobs +1. json API diff --git a/config/config.go b/config/config.go index 7094fda..47c03f7 100755 --- a/config/config.go +++ b/config/config.go @@ -17,6 +17,7 @@ var ( StoreUser string StorePass string Root string + Config string ) func init() { @@ -36,6 +37,7 @@ func Refresh() { as.Append(args.STRING, "storeuser", "storage username", "") as.Append(args.STRING, "storepass", "storage password", "") as.Append(args.STRING, "root", "root for static files", "./public") + as.Append(args.STRING, "config", "cron config to load;; non-persisting", "") if err := as.Parse(); err != nil { panic(err) } @@ -47,6 +49,11 @@ func Refresh() { StoreUser = as.Get("storeuser").GetString() StorePass = as.Get("storepass").GetString() Root = as.Get("root").GetString() + Config = as.Get("config").GetString() + + if Config != "" { + StoreType = "map" + } if db, err := storage.New(storage.TypeFromString(StoreType), StoreAddr, StoreUser, StorePass); err != nil { panic(err) diff --git a/logger/logger_test.go b/logger/logger_test.go index 31f4409..ac2c297 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -3,6 +3,7 @@ package logger import ( "bytes" "errors" + "fmt" "io/ioutil" "local/logb" "os" @@ -29,8 +30,8 @@ func TestInterface(t *testing.T) { os.Stderr = was }() - logger.Info("hello from %v", "me") - logger.Error(errors.New("bad"), "error from %v", "me") + logger.Info(fmt.Sprintf("hello from %v", "me")) + logger.Error(errors.New("bad"), fmt.Sprintf("error from %v", "me")) if !bytes.Contains(w.Bytes(), []byte(`error from me`)) { t.Errorf("%s", w.Bytes()) diff --git a/scheduler/job.go b/scheduler/job.go index 7224ee0..dc07d8c 100644 --- a/scheduler/job.go +++ b/scheduler/job.go @@ -3,6 +3,7 @@ package scheduler import ( "bytes" "encoding/gob" + "fmt" "local/firestormy/logger" "os/exec" @@ -41,7 +42,7 @@ func newBashJob(schedule, sh string) (*Job, error) { if err != nil { panic(err) } - logger.New().Info("executed %s: %s", sh, out) + logger.New().Info(fmt.Sprintf("executed %s: %s", sh, out)) }, }, nil } diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 591b8b6..7d7199e 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -1,9 +1,14 @@ package scheduler import ( + "bytes" + "errors" "fmt" + "io/ioutil" "local/firestormy/config" "local/firestormy/logger" + "regexp" + "strings" "time" cron "github.com/robfig/cron/v3" @@ -30,6 +35,61 @@ func New() *Scheduler { } } +func NewFromFile(config string) (*Scheduler, error) { + f, err := ioutil.ReadFile(config) + if err != nil { + return nil, err + } + s := New() + for _, line := range bytes.Split(f, []byte("\n")) { + line = cleanLine(line) + if len(line) == 0 { + continue + } + schedule, command := splitScheduleCommand(line) + if len(schedule) == 0 || len(command) == 0 { + continue + } + job, err := NewJob(Bash, schedule, command) + if err != nil { + logger.New().Error(err, "cannot fully parse file: new job error", config, ", sched", schedule, ", comm", command) + continue + } + if err := s.Add(job); err != nil { + logger.New().Error(err, "cannot fully parse file: add job error", config) + continue + } + } + jobs, _ := s.List() + if len(jobs) == 0 { + return nil, errors.New("no jobs parsed from file " + config) + } + return s, nil +} + +func cleanLine(b []byte) []byte { + b = bytes.Trim(b, "\t \n") + if len(b) == 0 { + return nil + } + if b[0] == '#' { + return nil + } + return b +} + +func splitScheduleCommand(b []byte) (string, string) { + re := regexp.MustCompile(`^((\d+|\*\/\d+|(\d,)*\d+|\*) [ ]*){5}`) + schedule := string(re.Find(b)) + if len(schedule) == 0 { + return "", "" + } + command := strings.TrimPrefix(string(b), schedule) + schedule = strings.TrimSpace(schedule) + command = strings.TrimSpace(command) + return schedule, command +} + func (s *Scheduler) Start() error { jobs, err := s.List() if err != nil { diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go index 86eb2a2..11036cb 100644 --- a/scheduler/scheduler_test.go +++ b/scheduler/scheduler_test.go @@ -2,8 +2,10 @@ package scheduler import ( "bytes" + "io/ioutil" "local/firestormy/config" "local/storage" + "os" "testing" ) @@ -59,3 +61,91 @@ func TestSchedulerStartStop(t *testing.T) { t.Errorf("%v: %s", n, b.Bytes()) } } + +func TestSchedulerFromFile(t *testing.T) { + was := config.Store + defer func() { + config.Store = was + }() + cases := map[string]struct { + content string + want int + }{ + "just a job": { + content: `10 */12 * * * /bin/bash -c "hostname"`, + want: 1, + }, + "all wild": { + content: `* * * * * /bin/bash -c "hostname"`, + want: 1, + }, + "all single numbers": { + content: `1 1 1 1 1 /bin/bash -c "hostname"`, + want: 1, + }, + "all double numbers": { + content: `10 10 10 10 2 /bin/bash -c "hostname"`, + want: 1, + }, + "all /\\d+": { + content: `*/11 */2 */3 */4 */1 /bin/bash -c "hostname"`, + want: 1, + }, + "2 jobs with 1 comment no whitespace leading": { + content: `# this is my comment + 10 */12 * * * /bin/bash -c "hostname" + 10 */12 * * * /bin/bash -c "hostname" + `, + want: 2, + }, + "2 jobs with 1 comment whitespace leading": { + content: ` # this is my comment + 10 */12 * * * /bin/bash -c "hostname" + 10 */12 * * * /bin/bash -c "hostname" + `, + want: 2, + }, + "2 jobs with crazy whitespace between cron spec": { + content: ` # this is my comment + 10 */12 * * * /bin/bash -c "hostname" + 10 */12 * * * /bin/bash -c "hostname" + `, + want: 2, + }, + "2 jobs with 2 comemnts and 2 empty lines": { + content: ` # this is my comment + + # this is a second comment + 10 */12 * * * /bin/bash -c "hostname" + + 10 */12 * * * /bin/bash -c "hostname" + `, + want: 2, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + config.Store, _ = storage.New(storage.MAP) + f, err := ioutil.TempFile(os.TempDir(), "testSchedulerFromFile") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Write([]byte(c.content)) + f.Close() + + s, err := NewFromFile(f.Name()) + if err != nil { + t.Fatal(err) + } + jobs, err := s.List() + if err != nil { + t.Fatal(err) + } + if len(jobs) != c.want { + t.Fatalf("want %v, got %v jobs", c.want, jobs) + } + }) + } +}