package main import ( "flag" "io/ioutil" "log" "math/rand" "os" "os/signal" "path" "strconv" "strings" "syscall" "time" "srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/settings" "gopkg.in/fsnotify.v1" ) var TeamsDir string var SubmissionDir string func watchsubdir(watcher *fsnotify.Watcher, pathname string) error { log.Println("Watch new directory:", pathname) if err := watcher.Add(pathname); err != nil { return err } if ds, err := ioutil.ReadDir(pathname); err != nil { return err } else { for _, d := range ds { p := path.Join(pathname, d.Name()) if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 { if err := watchsubdir(watcher, p); err != nil { return err } } else if d.Mode().IsRegular() { go treat(p) } } return nil } } func walkAndTreat(pathname string) error { if ds, err := ioutil.ReadDir(pathname); err != nil { return err } else { for _, d := range ds { p := path.Join(pathname, d.Name()) if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 { if err := walkAndTreat(p); err != nil { return err } } else if d.Mode().IsRegular() { treat(p) } } return nil } } var ChStarted = false var lastRegeneration time.Time var skipInitialGeneration = false func reloadSettings(config *settings.Settings) { allowRegistration = config.AllowRegistration canJoinTeam = config.CanJoinTeam denyTeamCreation = config.DenyTeamCreation fic.HintCoefficient = config.HintCurCoefficient fic.WChoiceCoefficient = config.WChoiceCurCoefficient fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0 if lastRegeneration != config.Generation || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness { fic.PartialValidation = config.PartialValidation fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth fic.DisplayAllFlags = config.DisplayAllFlags fic.FirstBlood = config.FirstBlood fic.SubmissionCostBase = config.SubmissionCostBase fic.SubmissionUniqueness = config.SubmissionUniqueness fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries if !skipInitialGeneration { log.Println("Generating files...") go func() { genAll() log.Println("Full generation done") }() } else { skipInitialGeneration = false log.Println("Regeneration skipped by option.") } lastRegeneration = config.Generation } else { log.Println("No change found. Skipping regeneration.") } } func main() { if v, exists := os.LookupEnv("FIC_BASEURL"); exists { fic.FilesDir = v + "files" } else { fic.FilesDir = "/files" } var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server") flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings") flag.StringVar(&SubmissionDir, "submission", "./submissions", "Base directory where save submissions") flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") flag.StringVar(&fic.FilesDir, "files", fic.FilesDir, "Request path prefix to reach files") var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") flag.BoolVar(&skipInitialGeneration, "skipfullgeneration", skipInitialGeneration, "Skip the initial regeneration") flag.IntVar(¶llelJobs, "jobs", parallelJobs, "Number of generation workers") flag.Parse() log.SetPrefix("[backend] ") settings.SettingsDir = path.Clean(settings.SettingsDir) SubmissionDir = path.Clean(SubmissionDir) TeamsDir = path.Clean(TeamsDir) rand.Seed(time.Now().UnixNano()) launchWorkers() log.Println("Creating submission directory...") if _, err := os.Stat(path.Join(SubmissionDir, ".tmp")); os.IsNotExist(err) { if err := os.MkdirAll(path.Join(SubmissionDir, ".tmp"), 0777); err != nil { log.Fatal("Unable to create submission directory: ", err) } } log.Println("Opening DB...") if err := fic.DBInit(*dsn); err != nil { log.Fatal("Cannot open the database: ", err) } defer fic.DBClose() // Load configuration settings.LoadAndWatchSettings(path.Join(settings.SettingsDir, settings.SettingsFile), reloadSettings) log.Println("Registering directory events...") watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() if err := watchsubdir(watcher, SubmissionDir); err != nil { log.Fatal(err) } // Register SIGUSR1, SIGUSR2 interrupt1 := make(chan os.Signal, 1) signal.Notify(interrupt1, syscall.SIGUSR1) interrupt2 := make(chan os.Signal, 1) signal.Notify(interrupt2, syscall.SIGUSR2) watchedNotify := fsnotify.Create for { select { case <-interrupt1: log.Println("SIGUSR1 received, retreating all files in queue...") walkAndTreat(SubmissionDir) log.Println("SIGUSR1 treated.") case <-interrupt2: inQueueMutex.Lock() log.Printf("SIGUSR2 received, dumping statistics:\n parallelJobs: %d\n genTeamQueue size: %d\n genQueue: %d\n Teams in queue: %v\n Challenge started: %v\n Last regeneration: %v\n", parallelJobs, len(genTeamQueue), len(genQueue), inGenQueue, ChStarted, lastRegeneration) inQueueMutex.Unlock() case ev := <-watcher.Events: if d, err := os.Lstat(ev.Name); err == nil && ev.Op&fsnotify.Create == fsnotify.Create && d.Mode().IsDir() && d.Mode()&os.ModeSymlink == 0 && d.Name() != ".tmp" { // Register new subdirectory if err := watchsubdir(watcher, ev.Name); err != nil { log.Println(err) } } else if ev.Op&watchedNotify == watchedNotify && d.Mode().IsRegular() { if *debugINotify { log.Println("Treating event:", ev, "for", ev.Name) } go treat(ev.Name) } else if 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 } else if *debugINotify { log.Println("Skipped event:", ev, "for", ev.Name) } case err := <-watcher.Errors: log.Println("error:", err) } } } func treat(raw_path string) { // Extract spath := strings.Split(strings.TrimPrefix(raw_path, SubmissionDir), "/") if len(spath) == 3 { if spath[1] == "_registration" { treatRegistration(raw_path, spath[2]) return } var teamid int64 var err error if teamid, err = strconv.ParseInt(spath[1], 10, 64); err != nil { if lnk, err := os.Readlink(path.Join(TeamsDir, spath[1])); err != nil { log.Printf("[ERR] Unable to readlink %q: %s\n", path.Join(TeamsDir, spath[1]), err) return } else if teamid, err = strconv.ParseInt(lnk, 10, 64); err != nil { log.Printf("[ERR] Error during ParseInt team %q: %s\n", lnk, err) return } } var team *fic.Team if team, err = fic.GetTeam(teamid); err != nil { log.Printf("[ERR] Unable to retrieve team %d: %s\n", teamid, err) return } switch spath[2] { case "name": treatRename(raw_path, team) case "issue": treatIssue(raw_path, team) case "hint": treatOpeningHint(raw_path, team) case "choices": treatWantChoices(raw_path, team) default: treatSubmission(raw_path, team, spath[2]) } } else { log.Println("Invalid new file:", raw_path) } }