// 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 (if empty their is no end-date). End *time.Time `json:"end,omitempty"` // NextChangeTime is the time of the next expected reload. NextChangeTime *time.Time `json:"nextchangetime,omitempty"` // 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"` // DiscountedFactor stores the percentage of the exercice's gain lost on each validation. DiscountedFactor float64 `json:"discountedFactor,omitempty"` // 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"` // CanResetProgression allows a team to reset the progress it made on a given exercice. CanResetProgression bool `json:"canResetProgress,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"` // UnlockedChallengeUpTo unlock challenge up to a given level of deps. UnlockedChallengeUpTo int `json:"unlockedChallengeUpTo"` // UnlockedStandaloneExercices unlock this number of standalone exercice. UnlockedStandaloneExercices int `json:"unlockedStandaloneExercices,omitempty"` // UnlockedStandaloneExercicesByThemeStepValidation unlock this number of standalone exercice for each theme step validated. UnlockedStandaloneExercicesByThemeStepValidation float64 `json:"unlockedStandaloneExercicesByThemeStepValidation,omitempty"` // UnlockedStandaloneExercicesByStandaloneExerciceValidation unlock this number of standalone exercice for each standalone exercice validated. UnlockedStandaloneExercicesByStandaloneExerciceValidation float64 `json:"unlockedStandaloneExercicesByStandaloneExerciceValidation,omitempty"` // 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"` // HideCaseSensitivity never tells the user if the flag is case sensitive or not. HideCaseSensitivity bool `json:"hideCaseSensitivity,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"` // DisableSubmitButton replace button by this text (eg. scheduled updates, ...). DisableSubmitButton string `json:"disablesubmitbutton,omitempty"` // GlobalTopMessage display a message on top of each pages. GlobalTopMessage string `json:"globaltopmessage,omitempty"` // GlobalTopMessageVariant control the variant/color of the previous message. GlobalTopMessageVariant string `json:"globaltopmessagevariant,omitempty"` // HideHeader will hide the countdown and partners block on front pages. HideHeader bool `json:"hide_header,omitempty"` // DelegatedQA contains the users allowed to perform administrative tasks on the QA platform. DelegatedQA []string `json:"delegated_qa,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 } } // 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 { reload(config) } }