evdist: Retactor + include dashboard lookup

This commit is contained in:
nemunaire 2024-03-14 17:43:51 +01:00
parent d44fc4f715
commit 5592fabefa
6 changed files with 338 additions and 180 deletions

92
evdist/dashboard.go Normal file
View File

@ -0,0 +1,92 @@
package main
import (
"fmt"
"log"
"os"
"path"
"strconv"
"strings"
"time"
)
var DashboardDir = "DASHBOARD"
type NextDashboardFile struct {
Id int64 `json:"id"`
Name string `json:"name"`
Screen int `json:"screen"`
Date time.Time `json:"date"`
}
func (ndf *NextDashboardFile) GetId() int64 {
return ndf.Id
}
func (ndf *NextDashboardFile) GetDate() *time.Time {
return &ndf.Date
}
// NewDashboardDistList creates a distList from the given src directory
func NewDashboardDistList(src string) (*distList, error) {
var list []DistEvent
files, err := os.ReadDir(DashboardDir)
if err != nil {
return nil, err
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), "public") || len(file.Name()) < 18 {
continue
}
ts, err := strconv.ParseInt(file.Name()[8:18], 10, 64)
if err == nil {
s, _ := strconv.Atoi(file.Name()[6:7])
list = append(list, &NextDashboardFile{
Id: ts * int64(s),
Name: file.Name(),
Screen: s,
Date: time.Unix(ts, 0),
})
}
}
return &distList{List: list, Timer: time.NewTimer(time.Minute)}, nil
}
func parseDashboardFilename(fname string) (int64, error) {
return strconv.ParseInt(fname[8:18], 10, 64)
}
func newDashboardFile(l *distList, raw_path string) {
bpath := path.Base(raw_path)
if !strings.HasPrefix(bpath, "public") || len(bpath) < 18 {
return
}
if ts, err := parseSettingsFilename(bpath); err == nil {
activateTime := time.Unix(ts, 0)
log.Printf("Preparing %s: activation time at %s", bpath, activateTime)
s, _ := strconv.Atoi(bpath[6:7])
l.AddEvent(&NextDashboardFile{
Id: ts * int64(s),
Name: bpath,
Screen: s,
Date: activateTime,
})
} else {
log.Println("WARNING: Unknown file to treat: not a valid timestamp:", err.Error())
}
}
func treatDashboardFile(e *NextDashboardFile) {
err := os.Rename(path.Join(DashboardDir, e.Name), path.Join(DashboardDir, fmt.Sprintf("public%d.json", e.Screen)))
if err != nil {
log.Printf("Unable to move %q: %s", e.Name, err.Error())
}
}

124
evdist/distlist.go Normal file
View File

@ -0,0 +1,124 @@
package main
import (
"log"
"sync"
"time"
)
type DistEvent interface {
GetId() int64
GetDate() *time.Time
}
// distList maintain a nextSettingsFile list up-to-date.
type distList struct {
List []DistEvent
Lock sync.RWMutex
Timer *time.Timer
}
func (l *distList) TimerNextEvent() *time.Timer {
l.Lock.RLock()
defer l.Lock.RUnlock()
var min *time.Time
for _, f := range l.List {
if min == nil || f.GetDate().Before(*min) {
min = f.GetDate()
}
}
if min == nil {
return nil
}
return time.NewTimer(time.Until(*min))
}
func (l *distList) AddEvent(nsf DistEvent) {
l.Lock.Lock()
istop := len(l.List)
for i, n := range l.List {
if n.GetId() == nsf.GetId() {
l.Lock.Unlock()
return
} else if n.GetDate().After(*nsf.GetDate()) {
istop = i
break
}
}
l.List = append(l.List, nsf)
copy(l.List[istop+1:], l.List[istop:])
l.List[istop] = nsf
l.Lock.Unlock()
if istop == 0 {
l.ResetTimer()
}
}
func (l *distList) DelEvent(id int64) {
l.Lock.Lock()
istop := len(l.List)
for i, n := range l.List {
if n.GetId() == id {
istop = i
break
}
}
if istop == len(l.List)-1 {
l.List = l.List[:istop]
} else if istop != len(l.List) {
l.List = append(l.List[:istop], l.List[istop+1:]...)
}
l.Lock.Unlock()
if istop == 0 {
l.ResetTimer()
}
}
func (l *distList) ResetTimer() {
l.Lock.RLock()
defer l.Lock.RUnlock()
if len(l.List) == 0 {
l.Timer.Reset(time.Minute)
} else {
l.Timer.Reset(time.Until(*l.List[0].GetDate()))
}
}
func (l *distList) Pop() DistEvent {
l.Lock.Lock()
defer l.Lock.Unlock()
if len(l.List) == 0 {
return nil
}
if time.Now().Before(*l.List[0].GetDate()) {
return nil
}
ret := l.List[0]
l.List = l.List[1:]
return ret
}
func (l *distList) Print() {
l.Lock.RLock()
defer l.Lock.RUnlock()
for n, i := range l.List {
log.Printf("#%d: %v", n, i)
}
}

View File

@ -2,13 +2,10 @@ package main
import ( import (
"flag" "flag"
"fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
@ -21,7 +18,7 @@ var SettingsDistDir = "./SETTINGSDIST/"
var TmpSettingsDirectory string var TmpSettingsDirectory string
var TmpSettingsDistDirectory string var TmpSettingsDistDirectory string
func watchsubdir(l *distList, watcher *fsnotify.Watcher, pathname string) error { func watchsubdir(l *distList, watcher *fsnotify.Watcher, pathname string, treat func(l *distList, pathname string)) error {
log.Println("Watch new directory:", pathname) log.Println("Watch new directory:", pathname)
if err := watcher.Add(pathname); err != nil { if err := watcher.Add(pathname); err != nil {
return err return err
@ -33,7 +30,7 @@ func watchsubdir(l *distList, watcher *fsnotify.Watcher, pathname string) error
for _, d := range ds { for _, d := range ds {
p := path.Join(pathname, d.Name()) p := path.Join(pathname, d.Name())
if d.Mode().IsRegular() && d.Name() != ".tmp" { if d.Mode().IsRegular() && d.Name() != ".tmp" {
l.treat(p) treat(l, p)
} }
} }
return nil return nil
@ -41,6 +38,7 @@ func watchsubdir(l *distList, watcher *fsnotify.Watcher, pathname string) error
} }
func main() { func main() {
flag.StringVar(&DashboardDir, "dashboard", DashboardDir, "Base directory where read dashboard files")
flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where read settings") flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where read settings")
flag.StringVar(&SettingsDistDir, "settingsDist", SettingsDistDir, "Directory where place settings to distribute") flag.StringVar(&SettingsDistDir, "settingsDist", SettingsDistDir, "Directory where place settings to distribute")
var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events")
@ -48,6 +46,7 @@ func main() {
log.SetPrefix("[evdist] ") log.SetPrefix("[evdist] ")
DashboardDir = path.Clean(DashboardDir)
settings.SettingsDir = path.Clean(settings.SettingsDir) settings.SettingsDir = path.Clean(settings.SettingsDir)
log.Println("Creating settingsDist directory...") log.Println("Creating settingsDist directory...")
@ -72,58 +71,73 @@ func main() {
} }
defer watcher.Close() defer watcher.Close()
l := &distList{} lDashboard := &distList{}
l.Timer = time.NewTimer(time.Minute) lDashboard.Timer = time.NewTimer(time.Minute)
if err := watchsubdir(l, watcher, settings.SettingsDir); err != nil { if err := watchsubdir(lDashboard, watcher, DashboardDir, newDashboardFile); err != nil {
log.Fatal(err)
}
lSettings := &distList{}
lSettings.Timer = time.NewTimer(time.Minute)
if err := watchsubdir(lSettings, watcher, settings.SettingsDir, newSettingsFile); err != nil {
log.Fatal(err) log.Fatal(err)
} }
watchedNotify := fsnotify.Create watchedNotify := fsnotify.Create
treatNewFile := func(name string) {
if strings.HasPrefix(name, DashboardDir) {
newDashboardFile(lDashboard, name)
} else {
newSettingsFile(lSettings, name)
}
}
for { for {
select { select {
case <-l.Timer.C: case <-lSettings.Timer.C:
if v := l.Pop(); v != nil { if v := lSettings.Pop(); v != nil {
log.Printf("TREATING DIFF: %v", v) log.Printf("TREATING SETTINGS DIFF: %v", v)
treatSettingsFile(v)
v, err = settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", v.Id)), v.Id) }
if err != nil { lSettings.ResetTimer()
log.Printf("Unable to read json: %s", err.Error()) case <-lDashboard.Timer.C:
} else if cur_settings, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { if v := lDashboard.Pop(); v != nil {
log.Printf("Unable to read settings.json: %s", err.Error()) log.Printf("TREATING DASHBOARD DIFF: %v", v)
} else { if ndf, ok := v.(*NextDashboardFile); ok {
cur_settings = settings.MergeSettings(*cur_settings, v.Values) treatDashboardFile(ndf)
if err = settings.SaveSettings(path.Join(TmpSettingsDirectory, "settings.json"), cur_settings); err != nil {
log.Printf("Unable to save settings.json to tmp dir: %s", err.Error())
} else if err = os.Rename(path.Join(TmpSettingsDirectory, "settings.json"), path.Join(settings.SettingsDir, "settings.json")); err != nil {
log.Printf("Unable to move settings.json to dest dir: %s", err.Error())
} else if err = os.Remove(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", v.Id))); err != nil {
log.Printf("Unable to remove initial diff file (%d.json): %s", v.Id, err.Error())
}
} }
} }
l.ResetTimer() lDashboard.ResetTimer()
case ev := <-watcher.Events: case ev := <-watcher.Events:
if d, err := os.Lstat(ev.Name); err == nil && ev.Op&watchedNotify == watchedNotify && d.Name() != ".tmp" && d.Mode().IsRegular() { if d, err := os.Lstat(ev.Name); err == nil && ev.Op&watchedNotify == watchedNotify && d.Name() != ".tmp" && d.Mode().IsRegular() {
if *debugINotify { if *debugINotify {
log.Println("Treating event:", ev, "for", ev.Name) log.Println("Treating event:", ev, "for", ev.Name)
} }
l.treat(ev.Name) treatNewFile(ev.Name)
} else if err == nil && ev.Op&watchedNotify == fsnotify.Remove && d.Mode().IsRegular() { } else if ev.Op&fsnotify.Remove == fsnotify.Remove {
if *debugINotify { if *debugINotify {
log.Println("Treating deletion event:", ev, "for", ev.Name) log.Println("Treating deletion event:", ev, "for", ev.Name)
} }
if ts, err := strconv.ParseInt(strings.TrimSuffix(path.Base(ev.Name), ".json"), 10, 64); err == nil { if strings.HasPrefix(ev.Name, DashboardDir) {
log.Println("Unable to parseint", ev.Name, err.Error()) if ts, err := parseDashboardFilename(ev.Name); err == nil {
log.Println("Unable to parseint", ev.Name, err.Error())
} else {
lDashboard.DelEvent(ts)
}
} else { } else {
l.DelEvent(ts) if ts, err := parseSettingsFilename(ev.Name); err == nil {
log.Println("Unable to parseint", ev.Name, err.Error())
} else {
lSettings.DelEvent(ts)
}
} }
} else if err == nil && ev.Op&fsnotify.Write == fsnotify.Write { } else if err == nil && ev.Op&fsnotify.Write == fsnotify.Write {
log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.") log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.")
watchedNotify = fsnotify.Write watchedNotify = fsnotify.Write
l.treat(ev.Name) treatNewFile(ev.Name)
} else if *debugINotify { } else if *debugINotify {
log.Println("Skipped event:", ev, "for", ev.Name) log.Println("Skipped event:", ev, "for", ev.Name)
} }
@ -132,50 +146,3 @@ func main() {
} }
} }
} }
func (l *distList) treat(raw_path string) {
bpath := path.Base(raw_path)
if bpath == "challenge.json" || bpath == "settings.json" {
log.Printf("Copying %s to SETTINGDIST...", bpath)
// Copy content through tmp file
fd, err := os.Open(raw_path)
if err != nil {
log.Printf("ERROR: Unable to open %s: %s", raw_path, err.Error())
return
}
defer fd.Close()
tmpfile, err := ioutil.TempFile(TmpSettingsDistDirectory, "")
if err != nil {
log.Printf("ERROR: Unable to create temporary file for %s: %s", bpath, err.Error())
return
}
_, err = io.Copy(tmpfile, fd)
tmpfile.Close()
if err != nil {
log.Printf("ERROR: Unable to copy content to temporary file (%s): %s", bpath, err.Error())
return
}
os.Chmod(tmpfile.Name(), 0644)
if err = os.Rename(tmpfile.Name(), path.Join(SettingsDistDir, bpath)); err != nil {
log.Println("ERROR: Unable to move file:", err)
return
}
} else if ts, err := strconv.ParseInt(strings.TrimSuffix(bpath, ".json"), 10, 64); err == nil {
activateTime := time.Unix(ts, 0)
log.Printf("Preparing %s: activation time at %s", bpath, activateTime)
l.AddEvent(&settings.NextSettingsFile{
Id: ts,
Date: activateTime,
})
} else {
log.Println("WARNING: Unknown file to treat: not a valid timestamp:", err.Error())
}
}

View File

@ -1,134 +1,100 @@
package main package main
import ( import (
"fmt"
"io"
"io/ioutil"
"log" "log"
"sync" "os"
"path"
"strconv"
"strings"
"time" "time"
"srs.epita.fr/fic-server/settings" "srs.epita.fr/fic-server/settings"
) )
// distList maintain a nextSettingsFile list up-to-date.
type distList struct {
List []*settings.NextSettingsFile
Lock sync.RWMutex
Timer *time.Timer
}
// NewDistList creates a distList from the given src directory // NewDistList creates a distList from the given src directory
func NewDistList(src string) (*distList, error) { func NewSettingsDistList(src string) (*distList, error) {
list, err := settings.ListNextSettingsFiles() list, err := settings.ListNextSettingsFiles()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &distList{List: list, Timer: time.NewTimer(time.Minute)}, nil
var dlist []DistEvent
for _, e := range list {
dlist = append(dlist, e)
}
return &distList{List: dlist, Timer: time.NewTimer(time.Minute)}, nil
} }
func (l *distList) TimerNextEvent() *time.Timer { func parseSettingsFilename(fname string) (int64, error) {
l.Lock.RLock() return strconv.ParseInt(strings.TrimSuffix(fname, ".json"), 10, 64)
defer l.Lock.RUnlock()
var min *time.Time
for _, f := range l.List {
if min == nil || f.Date.Before(*min) {
min = &f.Date
}
}
if min == nil {
return nil
}
if min == nil {
return nil
}
return time.NewTimer(time.Until(*min))
} }
func (l *distList) AddEvent(nsf *settings.NextSettingsFile) { func newSettingsFile(l *distList, raw_path string) {
l.Lock.Lock() bpath := path.Base(raw_path)
istop := len(l.List) if bpath == "challenge.json" || bpath == "settings.json" {
for i, n := range l.List { log.Printf("Copying %s to SETTINGDIST...", bpath)
if n.Id == nsf.Id { // Copy content through tmp file
fd, err := os.Open(raw_path)
if err != nil {
log.Printf("ERROR: Unable to open %s: %s", raw_path, err.Error())
return return
} else if n.Date.After(nsf.Date) {
istop = i
break
} }
} defer fd.Close()
l.List = append(l.List, nsf) tmpfile, err := ioutil.TempFile(TmpSettingsDistDirectory, "")
copy(l.List[istop+1:], l.List[istop:]) if err != nil {
l.List[istop] = nsf log.Printf("ERROR: Unable to create temporary file for %s: %s", bpath, err.Error())
return
l.Lock.Unlock()
if istop == 0 {
l.ResetTimer()
}
}
func (l *distList) DelEvent(id int64) {
l.Lock.Lock()
istop := len(l.List)
for i, n := range l.List {
if n.Id == id {
istop = i
break
} }
}
if istop == len(l.List)-1 { _, err = io.Copy(tmpfile, fd)
l.List = l.List[:istop] tmpfile.Close()
} else if istop != len(l.List) {
l.List = append(l.List[:istop], l.List[istop+1:]...)
}
l.Lock.Unlock() if err != nil {
log.Printf("ERROR: Unable to copy content to temporary file (%s): %s", bpath, err.Error())
return
}
if istop == 0 { os.Chmod(tmpfile.Name(), 0644)
l.ResetTimer()
}
}
func (l *distList) ResetTimer() { if err = os.Rename(tmpfile.Name(), path.Join(SettingsDistDir, bpath)); err != nil {
l.Lock.RLock() log.Println("ERROR: Unable to move file:", err)
defer l.Lock.RUnlock() return
}
} else if ts, err := parseSettingsFilename(bpath); err == nil {
activateTime := time.Unix(ts, 0)
if len(l.List) == 0 { log.Printf("Preparing %s: activation time at %s", bpath, activateTime)
l.Timer.Reset(time.Minute)
l.AddEvent(&settings.NextSettingsFile{
Id: ts,
Date: activateTime,
})
} else { } else {
l.Timer.Reset(time.Until(l.List[0].Date)) log.Println("WARNING: Unknown file to treat: not a valid timestamp:", err.Error())
} }
} }
func (l *distList) Pop() *settings.NextSettingsFile { func treatSettingsFile(e DistEvent) {
l.Lock.Lock() v, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", e.GetId())), e.GetId())
defer l.Lock.Unlock() if err != nil {
log.Printf("Unable to read json: %s", err.Error())
} else if cur_settings, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
log.Printf("Unable to read settings.json: %s", err.Error())
} else {
cur_settings = settings.MergeSettings(*cur_settings, v.Values)
if len(l.List) == 0 { if err = settings.SaveSettings(path.Join(TmpSettingsDirectory, "settings.json"), cur_settings); err != nil {
return nil log.Printf("Unable to save settings.json to tmp dir: %s", err.Error())
} } else if err = os.Rename(path.Join(TmpSettingsDirectory, "settings.json"), path.Join(settings.SettingsDir, "settings.json")); err != nil {
log.Printf("Unable to move settings.json to dest dir: %s", err.Error())
if time.Now().Before(l.List[0].Date) { } else if err = os.Remove(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", v.Id))); err != nil {
return nil log.Printf("Unable to remove initial diff file (%d.json): %s", v.Id, err.Error())
} }
ret := l.List[0]
l.List = l.List[1:]
return ret
}
func (l *distList) Print() {
l.Lock.RLock()
defer l.Lock.RUnlock()
log.Println("Seeing distlist")
for n, i := range l.List {
log.Printf("#%d: %v", n, *i)
} }
} }

View File

@ -229,6 +229,7 @@ services:
image: nemunaire/fic-evdist:latest image: nemunaire/fic-evdist:latest
binds: binds:
- /etc/hosts:/etc/hosts:ro - /etc/hosts:/etc/hosts:ro
- /var/lib/fic/dashboard:/srv/DASHBOARD
- /var/lib/fic/settings:/srv/SETTINGS - /var/lib/fic/settings:/srv/SETTINGS
- /var/lib/fic/settingsdist:/srv/SETTINGSDIST - /var/lib/fic/settingsdist:/srv/SETTINGSDIST
net: new net: new

View File

@ -35,6 +35,14 @@ type NextSettingsFile struct {
Values map[string]interface{} `json:"values"` Values map[string]interface{} `json:"values"`
} }
func (nsf *NextSettingsFile) GetId() int64 {
return nsf.Id
}
func (nsf *NextSettingsFile) GetDate() *time.Time {
return &nsf.Date
}
func ReadNextSettingsFile(filename string, ts int64) (*NextSettingsFile, error) { func ReadNextSettingsFile(filename string, ts int64) (*NextSettingsFile, error) {
fd, err := os.Open(filename) fd, err := os.Open(filename)
if err != nil { if err != nil {