server/settings/settings.go

193 lines
7.0 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"
"syscall"
"time"
"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"
// Settings represents the settings panel.
type Settings struct {
// WorkInProgress indicates if the current challenge is under development or if it is in production.
WorkInProgress bool `json:"wip,omitempty"`
// 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"`
// GlobalScoreCoefficient is a coefficient to apply on display scores, not considered bonus.
GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"`
// AllowRegistration permits unregistered Team to register themselves.
AllowRegistration bool `json:"allowRegistration,omitempty"`
// CanJoinTeam permits unregistered account to join an already existing team.
CanJoinTeam bool `json:"canJoinTeam,omitempty"`
// DenyTeamCreation forces unregistered account to join a team, it's not possible to create a new team.
DenyTeamCreation bool `json:"denyTeamCreation,omitempty"`
// DenyNameChange disallow Team to change their name.
DenyNameChange bool `json:"denyNameChange,omitempty"`
// IgnoreTeamMembers don't ask team to have known members.
IgnoreTeamMembers bool `json:"ignoreTeamMembers,omitempty"`
// AcceptNewIssue enables the reporting system.
AcceptNewIssue bool `json:"acceptNewIssue,omitempty"`
// QAenabled enables links to QA interface.
QAenabled bool `json:"QAenabled,omitempty"`
// EnableResolutionRoute activates the route displaying resolution movies.
EnableResolutionRoute bool `json:"enableResolutionRoute,omitempty"`
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
PartialValidation bool `json:"partialValidation,omitempty"`
// 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,omitempty"`
// CountOnlyNotGoodTries don't count as a try when one good response is given at least.
CountOnlyNotGoodTries bool `json:"countOnlyNotGoodTries,omitempty"`
// DisplayAllFlags doesn't respect the predefined constraint existing between flags.
DisplayAllFlags bool `json:"displayAllFlags,omitempty"`
// DisplayMCQBadCount activates the report of MCQ bad responses counter.
DisplayMCQBadCount bool `json:"displayMCQBadCount,omitempty"`
// EventKindness will ask browsers to delay notification interval.
EventKindness bool `json:"eventKindness,omitempty"`
}
// 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) (*Settings, error) {
var s Settings
if fd, err := os.Open(path); err != nil {
return nil, 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 interface{}) 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(*Settings)) {
// 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(*Settings)) {
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)
}
}