183 lines
6.3 KiB
Go
183 lines
6.3 KiB
Go
// Package settings is shared across multiple services for easy parsing and
|
|
// retrieval of the challenge settings.
|
|
package settings
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"time"
|
|
"syscall"
|
|
|
|
"gopkg.in/fsnotify.v1"
|
|
)
|
|
|
|
// SettingsFile is the expected name of the file containing the settings.
|
|
const SettingsFile = "settings.json"
|
|
|
|
// SettingsDir is the relative location where the SettingsFile lies.
|
|
var SettingsDir string = "./SETTINGS"
|
|
|
|
// FICSettings represents the settings panel.
|
|
type FICSettings struct {
|
|
// Title is the displayed name of the challenge.
|
|
Title string `json:"title"`
|
|
// Authors is the group name of people making the challenge.
|
|
Authors string `json:"authors"`
|
|
// VideoLink is the link to explaination videos when the challenge is over.
|
|
VideosLink string `json:"videoslink"`
|
|
|
|
// Start is the departure time (expected or effective).
|
|
Start time.Time `json:"start"`
|
|
// End is the expected end time.
|
|
End time.Time `json:"end"`
|
|
// Generation is a value used to regenerate static files.
|
|
Generation time.Time `json:"generation"`
|
|
// ActivateTime is the time when the current file should be proceed.
|
|
ActivateTime time.Time `json:"activateTime"`
|
|
|
|
// FirstBlood is the coefficient applied to each first team who solve a challenge.
|
|
FirstBlood float64 `json:"firstBlood"`
|
|
// SubmissionCostBase is a complex number representing the cost of each attempts.
|
|
SubmissionCostBase float64 `json:"submissionCostBase"`
|
|
// ExerciceCurrentCoefficient is the current coefficient applied globaly to exercices.
|
|
ExerciceCurCoefficient float64 `json:"exerciceCurrentCoefficient"`
|
|
// HintCurrentCoefficient is the current coefficient applied to hint discovery.
|
|
HintCurCoefficient float64 `json:"hintCurrentCoefficient"`
|
|
// WChoiceCurCoefficient is the current coefficient applied to wanted choices.
|
|
WChoiceCurCoefficient float64 `json:"wchoiceCurrentCoefficient"`
|
|
|
|
// AllowRegistration permits unregistered Team to register themselves.
|
|
AllowRegistration bool `json:"allowRegistration"`
|
|
// CanJoinTeam permits unregistered account to join an already existing team.
|
|
CanJoinTeam bool `json:"canJoinTeam"`
|
|
// DenyNameChange disallow Team to change their name.
|
|
DenyNameChange bool `json:"denyNameChange"`
|
|
// EnableResolutionRoute activates the route displaying resolution movies.
|
|
EnableResolutionRoute bool `json:"enableResolutionRoute"`
|
|
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
|
|
PartialValidation bool `json:"partialValidation"`
|
|
// UnlockedChallengeDepth don't show (or permit to solve) to team challenges they are not unlocked through dependancies.
|
|
UnlockedChallengeDepth int `json:"unlockedChallengeDepth"`
|
|
// SubmissionUniqueness don't count multiple times identical tries.
|
|
SubmissionUniqueness bool `json:"submissionUniqueness"`
|
|
// DisplayAllFlags doesn't respect the predefined constraint existing between flags.
|
|
DisplayAllFlags bool `json:"displayAllFlags"`
|
|
// EventKindness will ask browsers to delay notification interval.
|
|
EventKindness bool `json:"eventKindness"`
|
|
}
|
|
|
|
// ExistsSettings checks if the settings file can by found at the given path.
|
|
func ExistsSettings(settingsPath string) bool {
|
|
_, err := os.Stat(settingsPath)
|
|
return !os.IsNotExist(err)
|
|
}
|
|
|
|
// ReadSettings parses the file at the given location.
|
|
func ReadSettings(path string) (FICSettings, error) {
|
|
var s FICSettings
|
|
if fd, err := os.Open(path); err != nil {
|
|
return s, err
|
|
} else {
|
|
defer fd.Close()
|
|
jdec := json.NewDecoder(fd)
|
|
|
|
if err := jdec.Decode(&s); err != nil {
|
|
return s, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
}
|
|
|
|
// SaveSettings saves settings at the given location.
|
|
func SaveSettings(path string, s FICSettings) error {
|
|
if fd, err := os.Create(path); err != nil {
|
|
return err
|
|
} else {
|
|
defer fd.Close()
|
|
jenc := json.NewEncoder(fd)
|
|
|
|
if err := jenc.Encode(s); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ForceRegeneration makes a small change to the settings structure in order to force the regeneration of all static files.
|
|
func ForceRegeneration() error {
|
|
location := path.Join(SettingsDir, SettingsFile)
|
|
if settings, err := ReadSettings(location); err != nil {
|
|
return err
|
|
} else {
|
|
settings.Generation = time.Now()
|
|
return SaveSettings(location, settings)
|
|
}
|
|
}
|
|
|
|
// LoadAndWatchSettings is the function you are looking for!
|
|
// Giving the location and a callback, this function will first call your reload function
|
|
// before returning (if the file can be parsed); then it starts watching modifications made to
|
|
// this file. Your callback is then run each time the file is modified.
|
|
func LoadAndWatchSettings(settingsPath string, reload func (FICSettings)) {
|
|
// First load of configuration if it exists
|
|
if _, err := os.Stat(settingsPath); !os.IsNotExist(err) {
|
|
if config, err := ReadSettings(settingsPath); err != nil {
|
|
log.Println("ERROR: Unable to read challenge settings:", err)
|
|
} else {
|
|
reload(config)
|
|
}
|
|
}
|
|
|
|
// Register SIGHUP
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, syscall.SIGHUP)
|
|
go func(){
|
|
for range c {
|
|
log.Println("SIGHUP received, reloading settings...")
|
|
go tryReload(settingsPath, reload)
|
|
}
|
|
}()
|
|
|
|
// Watch the configuration file
|
|
if watcher, err := fsnotify.NewWatcher(); err != nil {
|
|
log.Fatal(err)
|
|
} else {
|
|
if err := watcher.Add(path.Dir(settingsPath)); err != nil {
|
|
log.Fatal("Unable to watch: ", path.Dir(settingsPath), ": ", err)
|
|
}
|
|
|
|
go func() {
|
|
defer watcher.Close()
|
|
for {
|
|
select {
|
|
case ev := <-watcher.Events:
|
|
if path.Base(ev.Name) == SettingsFile && ev.Op & (fsnotify.Write | fsnotify.Create) != 0 {
|
|
log.Println("Settings file changes, reloading it!")
|
|
go tryReload(settingsPath, reload)
|
|
}
|
|
case err := <-watcher.Errors:
|
|
log.Println("watcher error:", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func tryReload(settingsPath string, reload func (FICSettings)) {
|
|
if config, err := ReadSettings(settingsPath); err != nil {
|
|
log.Println("ERROR: Unable to read challenge settings:", err)
|
|
} else if time.Until(config.ActivateTime) > 0 {
|
|
log.Println("Configuration reloading postponed, activating at:", config.ActivateTime)
|
|
time.Sleep(time.Until(config.ActivateTime))
|
|
log.Println("Time to activate configuration...")
|
|
tryReload(settingsPath, reload)
|
|
} else {
|
|
reload(config)
|
|
}
|
|
}
|