diff --git a/evdist/dashboard.go b/evdist/dashboard.go new file mode 100644 index 00000000..d60c82ea --- /dev/null +++ b/evdist/dashboard.go @@ -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()) + } +} diff --git a/evdist/distlist.go b/evdist/distlist.go new file mode 100644 index 00000000..c99cb0e6 --- /dev/null +++ b/evdist/distlist.go @@ -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) + } +} diff --git a/evdist/main.go b/evdist/main.go index 0866c1cf..f9a83842 100644 --- a/evdist/main.go +++ b/evdist/main.go @@ -2,13 +2,10 @@ package main import ( "flag" - "fmt" - "io" "io/ioutil" "log" "os" "path" - "strconv" "strings" "time" @@ -21,7 +18,7 @@ var SettingsDistDir = "./SETTINGSDIST/" var TmpSettingsDirectory 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) if err := watcher.Add(pathname); err != nil { return err @@ -33,7 +30,7 @@ func watchsubdir(l *distList, watcher *fsnotify.Watcher, pathname string) error for _, d := range ds { p := path.Join(pathname, d.Name()) if d.Mode().IsRegular() && d.Name() != ".tmp" { - l.treat(p) + treat(l, p) } } return nil @@ -41,6 +38,7 @@ func watchsubdir(l *distList, watcher *fsnotify.Watcher, pathname string) error } 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(&SettingsDistDir, "settingsDist", SettingsDistDir, "Directory where place settings to distribute") var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") @@ -48,6 +46,7 @@ func main() { log.SetPrefix("[evdist] ") + DashboardDir = path.Clean(DashboardDir) settings.SettingsDir = path.Clean(settings.SettingsDir) log.Println("Creating settingsDist directory...") @@ -72,58 +71,73 @@ func main() { } defer watcher.Close() - l := &distList{} - l.Timer = time.NewTimer(time.Minute) + lDashboard := &distList{} + 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) } watchedNotify := fsnotify.Create + treatNewFile := func(name string) { + if strings.HasPrefix(name, DashboardDir) { + newDashboardFile(lDashboard, name) + } else { + newSettingsFile(lSettings, name) + } + } + for { select { - case <-l.Timer.C: - if v := l.Pop(); v != nil { - log.Printf("TREATING DIFF: %v", v) - - v, err = settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", v.Id)), v.Id) - 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 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()) - } + case <-lSettings.Timer.C: + if v := lSettings.Pop(); v != nil { + log.Printf("TREATING SETTINGS DIFF: %v", v) + treatSettingsFile(v) + } + lSettings.ResetTimer() + case <-lDashboard.Timer.C: + if v := lDashboard.Pop(); v != nil { + log.Printf("TREATING DASHBOARD DIFF: %v", v) + if ndf, ok := v.(*NextDashboardFile); ok { + treatDashboardFile(ndf) } } - l.ResetTimer() + lDashboard.ResetTimer() case ev := <-watcher.Events: if d, err := os.Lstat(ev.Name); err == nil && ev.Op&watchedNotify == watchedNotify && d.Name() != ".tmp" && d.Mode().IsRegular() { if *debugINotify { log.Println("Treating event:", ev, "for", ev.Name) } - l.treat(ev.Name) - } else if err == nil && ev.Op&watchedNotify == fsnotify.Remove && d.Mode().IsRegular() { + treatNewFile(ev.Name) + } else if ev.Op&fsnotify.Remove == fsnotify.Remove { if *debugINotify { 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 { - log.Println("Unable to parseint", ev.Name, err.Error()) + if strings.HasPrefix(ev.Name, DashboardDir) { + if ts, err := parseDashboardFilename(ev.Name); err == nil { + log.Println("Unable to parseint", ev.Name, err.Error()) + } else { + lDashboard.DelEvent(ts) + } } 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 { log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.") watchedNotify = fsnotify.Write - l.treat(ev.Name) + treatNewFile(ev.Name) } else if *debugINotify { 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()) - } -} diff --git a/evdist/settings.go b/evdist/settings.go index fab363f9..a9011fa2 100644 --- a/evdist/settings.go +++ b/evdist/settings.go @@ -1,134 +1,100 @@ package main import ( + "fmt" + "io" + "io/ioutil" "log" - "sync" + "os" + "path" + "strconv" + "strings" "time" "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 -func NewDistList(src string) (*distList, error) { +func NewSettingsDistList(src string) (*distList, error) { list, err := settings.ListNextSettingsFiles() if err != nil { 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 { - l.Lock.RLock() - 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 parseSettingsFilename(fname string) (int64, error) { + return strconv.ParseInt(strings.TrimSuffix(fname, ".json"), 10, 64) } -func (l *distList) AddEvent(nsf *settings.NextSettingsFile) { - l.Lock.Lock() +func newSettingsFile(l *distList, raw_path string) { + bpath := path.Base(raw_path) - istop := len(l.List) - for i, n := range l.List { - if n.Id == nsf.Id { + 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 - } else if n.Date.After(nsf.Date) { - istop = i - break } - } + defer fd.Close() - 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.Id == id { - istop = i - break + tmpfile, err := ioutil.TempFile(TmpSettingsDistDirectory, "") + if err != nil { + log.Printf("ERROR: Unable to create temporary file for %s: %s", bpath, err.Error()) + return } - } - 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:]...) - } + _, err = io.Copy(tmpfile, fd) + tmpfile.Close() - 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 { - l.ResetTimer() - } -} + os.Chmod(tmpfile.Name(), 0644) -func (l *distList) ResetTimer() { - l.Lock.RLock() - defer l.Lock.RUnlock() + 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 := parseSettingsFilename(bpath); err == nil { + activateTime := time.Unix(ts, 0) - if len(l.List) == 0 { - l.Timer.Reset(time.Minute) + log.Printf("Preparing %s: activation time at %s", bpath, activateTime) + + l.AddEvent(&settings.NextSettingsFile{ + Id: ts, + Date: activateTime, + }) } 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 { - l.Lock.Lock() - defer l.Lock.Unlock() +func treatSettingsFile(e DistEvent) { + v, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", e.GetId())), e.GetId()) + 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 { - return nil - } - - if time.Now().Before(l.List[0].Date) { - return nil - } - - 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) + 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()) + } } } diff --git a/fickit-backend.yml b/fickit-backend.yml index ac8d4d8c..526e0568 100644 --- a/fickit-backend.yml +++ b/fickit-backend.yml @@ -229,6 +229,7 @@ services: image: nemunaire/fic-evdist:latest binds: - /etc/hosts:/etc/hosts:ro + - /var/lib/fic/dashboard:/srv/DASHBOARD - /var/lib/fic/settings:/srv/SETTINGS - /var/lib/fic/settingsdist:/srv/SETTINGSDIST net: new diff --git a/settings/diff.go b/settings/diff.go index 67d59f38..5e4296f2 100644 --- a/settings/diff.go +++ b/settings/diff.go @@ -35,6 +35,14 @@ type NextSettingsFile struct { 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) { fd, err := os.Open(filename) if err != nil {