feature: Exec on Calendar file support (#243)

* feature: Exec on calendar file

* feature: support for datetime args

* feature: Schedule commands based on seconds
This commit is contained in:
James Read 2024-03-08 20:31:51 +00:00 committed by GitHub
parent a992ef84f5
commit 2fb40ff443
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 231 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/OliveTin/OliveTin/internal/executor" "github.com/OliveTin/OliveTin/internal/executor"
grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi" grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
"github.com/OliveTin/OliveTin/internal/installationinfo" "github.com/OliveTin/OliveTin/internal/installationinfo"
"github.com/OliveTin/OliveTin/internal/oncalendarfile"
"github.com/OliveTin/OliveTin/internal/oncron" "github.com/OliveTin/OliveTin/internal/oncron"
"github.com/OliveTin/OliveTin/internal/onfileindir" "github.com/OliveTin/OliveTin/internal/onfileindir"
"github.com/OliveTin/OliveTin/internal/onstartup" "github.com/OliveTin/OliveTin/internal/onstartup"
@ -153,6 +154,7 @@ func main() {
go onstartup.Execute(cfg, executor) go onstartup.Execute(cfg, executor)
go oncron.Schedule(cfg, executor) go oncron.Schedule(cfg, executor)
go onfileindir.WatchFilesInDirectory(cfg, executor) go onfileindir.WatchFilesInDirectory(cfg, executor)
go oncalendarfile.Schedule(cfg, executor)
go entityfiles.SetupEntityFileWatchers(cfg) go entityfiles.SetupEntityFileWatchers(cfg)

View File

@ -17,6 +17,7 @@ type Action struct {
ExecOnCron []string ExecOnCron []string
ExecOnFileCreatedInDir []string ExecOnFileCreatedInDir []string
ExecOnFileChangedInDir []string ExecOnFileChangedInDir []string
ExecOnCalendarFile string
Trigger string Trigger string
MaxConcurrent int MaxConcurrent int
Arguments []ActionArgument Arguments []ActionArgument

View File

@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
"time"
) )
var ( var (
@ -94,6 +95,16 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
return typeSafetyCheckUrl(name, value) return typeSafetyCheckUrl(name, value)
} }
if argumentType == "datetime" {
_, err := time.Parse("2006-01-02T15:04:05", value)
if err != nil {
return err
}
return nil
}
return typeSafetyCheckRegex(name, value, argumentType) return typeSafetyCheckRegex(name, value, argumentType)
} }

View File

@ -0,0 +1,75 @@
package filehelper
import (
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"path/filepath"
)
func WatchFile(fullpath string, callback func()) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Errorf("Could not watch for files being created: %v", err)
return
}
defer watcher.Close()
done := make(chan bool)
filename := filepath.Base(fullpath)
filedir := filepath.Dir(fullpath)
go func() {
for {
processEvent(filename, watcher, fsnotify.Write, callback)
}
}()
err = watcher.Add(filedir)
if err != nil {
log.Errorf("Could not create watcher: %v", err)
}
<-done
}
func processEvent(filename string, watcher *fsnotify.Watcher, eventType fsnotify.Op, callback func()) {
select {
case event, ok := <-watcher.Events:
if !consumeEvent(ok, filename, &event, callback) {
return
}
break
case err := <-watcher.Errors:
log.Errorf("Error in fsnotify: %v", err)
return
}
}
func consumeEvent(ok bool, filename string, event *fsnotify.Event, callback func()) bool {
if !ok {
return false
}
if filepath.Base(event.Name) != filename {
log.Tracef("fsnotify irreleventa event different file %+v", event)
return true
}
consumeWriteEvents(event, callback)
return true
}
func consumeWriteEvents(event *fsnotify.Event, callback func()) {
if event.Has(fsnotify.Write) {
log.Debugf("fsnotify write event: %v", event)
callback()
} else {
log.Debugf("fsnotify irrelevant event on file %v", event)
}
}

View File

@ -0,0 +1,20 @@
package filehelper
import (
log "github.com/sirupsen/logrus"
"os"
)
func Touch(filename string, description string) {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
_, err := os.Create(filename)
if err != nil {
log.Warnf("Could not create %v: %v", description, filename)
} else {
log.Infof("Created %v: %v", description, filename)
}
}
}

View File

@ -0,0 +1,118 @@
package oncalendarfile
import (
"context"
"github.com/OliveTin/OliveTin/internal/acl"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/executor"
"github.com/OliveTin/OliveTin/internal/filehelper"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"io/ioutil"
"time"
)
var calendar map[*config.Action][]*time.Timer
func Schedule(cfg *config.Config, ex *executor.Executor) {
calendar = make(map[*config.Action][]*time.Timer)
for _, action := range cfg.Actions {
if action.ExecOnCalendarFile != "" {
x := func() {
parseCalendarFile(action, cfg, ex)
}
go filehelper.WatchFile(action.ExecOnCalendarFile, x)
x()
}
}
}
func parseCalendarFile(action *config.Action, cfg *config.Config, ex *executor.Executor) {
filehelper.Touch(action.ExecOnCalendarFile, "calendar file")
log.WithFields(log.Fields{
"actionTitle": action.Title,
}).Infof("Parsing calendar file")
yfile, err := ioutil.ReadFile(action.ExecOnCalendarFile)
if err != nil {
log.Errorf("ReadIn: %v", err)
return
}
data := make([]string, 1)
err = yaml.Unmarshal(yfile, &data)
if err != nil {
log.Errorf("Unmarshal: %v", err)
}
scheduleCalendarActions(data, action, cfg, ex)
}
func scheduleCalendarActions(entries []string, action *config.Action, cfg *config.Config, ex *executor.Executor) {
ctx := context.Background()
for _, instant := range entries {
if instant == "" {
continue
}
until, _ := time.Parse(time.RFC3339, instant)
go sleepUntil(ctx, until, action, cfg, ex)
}
}
func sleepUntil(ctx context.Context, instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) {
if time.Now().After(instant) {
log.WithFields(log.Fields{
"instant": instant,
"actionTitle": action.Title,
}).Warnf("Not scheduling stale calendar action")
return
}
log.WithFields(log.Fields{
"instant": instant,
"actionTitle": action.Title,
}).Infof("Scheduling action on calendar")
timer := time.NewTimer(time.Until(instant))
defer timer.Stop()
select {
case <-timer.C:
exec(instant, action, cfg, ex)
return
case <-ctx.Done():
log.Infof("Cancelled scheduled action")
return
}
}
func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) {
// calendar[action] = append(calendar[action], timer)
log.WithFields(log.Fields{
"instant": instant,
"actionTitle": action.Title,
}).Infof("Executing action from calendar")
req := &executor.ExecutionRequest{
Action: action,
Cfg: cfg,
Tags: []string{"calendar"},
AuthenticatedUser: &acl.AuthenticatedUser{
Username: "calendar",
},
}
ex.ExecRequest(req)
}

View File

@ -102,6 +102,10 @@ class ArgumentForm extends window.HTMLElement {
this.domBtnStart.disabled = false this.domBtnStart.disabled = false
domEl.disabled = true domEl.disabled = true
} }
} else if (arg.type === 'datetime') {
domEl = document.createElement('input')
domEl.setAttribute('type', 'datetime-local')
domEl.setAttribute('step', '1')
} else { } else {
domEl = document.createElement('input') domEl = document.createElement('input')
domEl.onchange = () => { domEl.onchange = () => {