diff --git a/admin/api/claim.go b/admin/api/claim.go index 6a958938..cf545fff 100644 --- a/admin/api/claim.go +++ b/admin/api/claim.go @@ -1,25 +1,21 @@ package api import ( - "bytes" "encoding/json" "fmt" "io/ioutil" "log" - "net" "net/http" "path" "strconv" - "strings" "time" + "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/libfic" "github.com/gin-gonic/gin" ) -var GeneratorSocket string - func declareClaimsRoutes(router *gin.RouterGroup) { // Tasks router.GET("/claims", getClaims) @@ -292,7 +288,7 @@ func clearClaims(c *gin.Context) { } func generateTeamIssuesFile(team fic.Team) error { - if GeneratorSocket == "" { + if generation.GeneratorSocket == "" { if my, err := team.MyIssueFile(); err != nil { return fmt.Errorf("Unable to generate issue FILE (tid=%d): %w", team.Id, err) } else if j, err := json.Marshal(my); err != nil { @@ -301,35 +297,16 @@ func generateTeamIssuesFile(team fic.Team) error { return fmt.Errorf("Unable to write issues' file: %w", err) } } else { - buf, err := json.Marshal(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id}) - if err != nil { - return fmt.Errorf("Something is wrong with JSON encoder: %w", err) - } - - sockType := "unix" - if strings.Contains(GeneratorSocket, ":") { - sockType = "tcp" - } - - socket, err := net.Dial(sockType, GeneratorSocket) + resp, err := generation.PerformGeneration(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id}) if err != nil { return err } - defer socket.Close() + defer resp.Body.Close() - httpClient := &http.Client{ - Transport: &http.Transport{ - Dial: func(network, addr string) (net.Conn, error) { - return socket, nil - }, - }, + if resp.StatusCode != http.StatusOK { + v, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("%s", string(v)) } - - resp, err := httpClient.Post("http://localhost/enqueue", "application/json", bytes.NewReader(buf)) - if err != nil { - return fmt.Errorf("Unable to enqueue new generation event: %w", err) - } - resp.Body.Close() } return nil } diff --git a/admin/api/settings.go b/admin/api/settings.go index 4213d37a..138a2d1b 100644 --- a/admin/api/settings.go +++ b/admin/api/settings.go @@ -12,6 +12,7 @@ import ( "strconv" "time" + "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/settings" @@ -47,6 +48,7 @@ func declareSettingsRoutes(router *gin.RouterGroup) { apiNextSettingsRoutes.DELETE("", deleteNextSettings) router.POST("/reset", reset) + router.POST("/full-generation", fullGeneration) router.GET("/prod", func(c *gin.Context) { c.JSON(http.StatusOK, IsProductionEnv) @@ -80,6 +82,21 @@ func NextSettingsHandler(c *gin.Context) { c.Next() } +func fullGeneration(c *gin.Context) { + resp, err := generation.FullGeneration() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "errmsg": err.Error(), + }) + } + defer resp.Body.Close() + + v, _ := io.ReadAll(resp.Body) + c.JSON(resp.StatusCode, gin.H{ + "errmsg": v, + }) +} + func getROSettings(c *gin.Context) { syncMtd := "Disabled" if sync.GlobalImporter != nil { diff --git a/admin/api/sync.go b/admin/api/sync.go index c0630ac2..0ef44ef2 100644 --- a/admin/api/sync.go +++ b/admin/api/sync.go @@ -8,9 +8,9 @@ import ( "path" "strings" + "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" - "srs.epita.fr/fic-server/settings" "github.com/gin-gonic/gin" ) @@ -292,7 +292,10 @@ func autoSync(c *gin.Context) { sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false) sync.DeepSyncProgress = 255 - settings.ForceRegeneration() + resp, err := generation.FullGeneration() + if err == nil { + defer resp.Body.Close() + } c.JSON(http.StatusOK, st) } diff --git a/admin/generation/generation.go b/admin/generation/generation.go new file mode 100644 index 00000000..ffbaafd9 --- /dev/null +++ b/admin/generation/generation.go @@ -0,0 +1,60 @@ +package generation + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + + "srs.epita.fr/fic-server/libfic" +) + +var GeneratorSocket string + +func doGeneration(uri string, contenttype string, buf io.Reader) (*http.Response, error) { + sockType := "unix" + if strings.Contains(GeneratorSocket, ":") { + sockType = "tcp" + } + + socket, err := net.Dial(sockType, GeneratorSocket) + if err != nil { + return nil, err + } + defer socket.Close() + + httpClient := &http.Client{ + Transport: &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + return socket, nil + }, + }, + } + + return httpClient.Post("http://localhost"+uri, contenttype, buf) +} + +func EnqueueGeneration(gs fic.GenStruct) (*http.Response, error) { + buf, err := json.Marshal(gs) + if err != nil { + return nil, fmt.Errorf("Something is wrong with JSON encoder: %w", err) + } + + return doGeneration("/enqueue", "application/json", bytes.NewReader(buf)) +} + +func PerformGeneration(gs fic.GenStruct) (*http.Response, error) { + buf, err := json.Marshal(gs) + if err != nil { + return nil, fmt.Errorf("Something is wrong with JSON encoder: %w", err) + } + + return doGeneration("/perform", "application/json", bytes.NewReader(buf)) +} + +func FullGeneration() (*http.Response, error) { + return doGeneration("/full", "application/json", nil) +} diff --git a/admin/index.go b/admin/index.go index 7cfeea3e..46c13c15 100644 --- a/admin/index.go +++ b/admin/index.go @@ -128,7 +128,13 @@ const indextpl = `
- + Démarrage dans : {{"{{ startIn }}"}}" diff --git a/admin/main.go b/admin/main.go index c932085e..f4913daf 100644 --- a/admin/main.go +++ b/admin/main.go @@ -14,6 +14,7 @@ import ( "syscall" "srs.epita.fr/fic-server/admin/api" + "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" @@ -108,7 +109,7 @@ func main() { flag.StringVar(&api.DashboardDir, "dashbord", "./DASHBOARD", "Base directory where save public JSON files") flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings") flag.StringVar(&fic.FilesDir, "files", fic.FilesDir, "Base directory where found challenges files, local part") - flag.StringVar(&api.GeneratorSocket, "generator", "./GENERATOR/generator.socket", "Path to the generator socket (used to trigger issues.json generations, use an empty string to generate locally)") + flag.StringVar(&generation.GeneratorSocket, "generator", "./GENERATOR/generator.socket", "Path to the generator socket (used to trigger issues.json generations, use an empty string to generate locally)") flag.StringVar(&localImporterDirectory, "localimport", localImporterDirectory, "Base directory where found challenges files to import, local part") flag.BoolVar(&localImporterSymlink, "localimportsymlink", localImporterSymlink, diff --git a/admin/static/js/app.js b/admin/static/js/app.js index bea958fd..c36d28b3 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -480,15 +480,16 @@ angular.module("FICApp") } $rootScope.staticFilesNeedUpdate = 0; + $rootScope.staticRegenerationInProgress = false; $rootScope.regenerateStaticFiles = function() { - Settings.get().$promise.then(function(config) { - config.generation = (new Date()).toISOString(); - config.$update(function() { - $rootScope.staticFilesNeedUpdate = 0; - $rootScope.addToast('success', "Regeneration in progress..."); - }, function (response) { - $rootScope.addToast('success', 'An error occurs when saving settings:', response.data.errmsg); - }) + $rootScope.staticRegenerationInProgress = true; + $http.post("api/full-generation").then(function(response) { + $rootScope.staticFilesNeedUpdate = 0; + $rootScope.staticRegenerationInProgress = false; + $rootScope.addToast('success', 'Regeneration ended'); + }, function (response) { + $rootScope.staticRegenerationInProgress = false; + $rootScope.addToast('error', 'An error occurs when saving settings:', response.data.errmsg); }) } @@ -633,11 +634,6 @@ angular.module("FICApp") $scope.addToast('danger', 'An error occurs when saving settings:', response.data.errmsg); }); } - $scope.regenerate = function() { - this.config.generation = (new Date()).toISOString(); - $rootScope.settings.generation = new Date(this.config.generation); - $scope.saveSettings("Regeneration in progress..."); - } $scope.launchChallenge = function() { var ts = Date.now() - Date.now() % 60000; var d = new Date(ts + 120000); diff --git a/admin/static/views/settings.html b/admin/static/views/settings.html index ca51cae1..9a0c9b06 100644 --- a/admin/static/views/settings.html +++ b/admin/static/views/settings.html @@ -31,8 +31,6 @@
- -
@@ -382,7 +380,13 @@
- +

diff --git a/admin/sync/full.go b/admin/sync/full.go index 7388b077..f8cf5fe5 100644 --- a/admin/sync/full.go +++ b/admin/sync/full.go @@ -2,13 +2,14 @@ package sync import ( "encoding/json" + "io" "log" "os" "sync" "time" + "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/libfic" - "srs.epita.fr/fic-server/settings" ) // DeepReportPath stores the path to the report generated during full recursive import. @@ -132,8 +133,14 @@ func SyncDeep(i Importer) (errs SyncReport) { EditDeepReport(&errs, true) - if err := settings.ForceRegeneration(); err != nil { + resp, err := generation.FullGeneration() + if err != nil { errs.Regeneration = append(errs.Regeneration, err.Error()) + } else { + defer resp.Body.Close() + + v, _ := io.ReadAll(resp.Body) + errs.Regeneration = append(errs.Regeneration, string(v)) } DeepSyncProgress = 255 diff --git a/generator/generation.go b/generator/generation.go index 2a08afd0..dfbfa4f3 100644 --- a/generator/generation.go +++ b/generator/generation.go @@ -10,17 +10,18 @@ import ( "path" "runtime" "sync" + "time" "srs.epita.fr/fic-server/libfic" ) var parallelJobs = runtime.NumCPU() -var genQueue chan fic.GenStruct +var genQueue chan *fic.GenStruct var inQueueMutex sync.RWMutex var inGenQueue map[fic.GenerateType]bool func init() { - genQueue = make(chan fic.GenStruct) + genQueue = make(chan *fic.GenStruct) inGenQueue = map[fic.GenerateType]bool{} } @@ -47,12 +48,54 @@ func enqueueHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "OK", http.StatusOK) } -func appendGenQueue(gs fic.GenStruct) { - if gs.Type == fic.GenTeam || gs.Type == fic.GenTeamIssues { - genQueue <- gs +func performHandler(w http.ResponseWriter, r *http.Request) { + var gs fic.GenStruct + + dec := json.NewDecoder(r.Body) + err := dec.Decode(&gs) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } + log.Printf("POST /perform | %v", gs) + + err = <-appendGenQueue(gs).GenEnded() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + http.Error(w, "OK", http.StatusOK) + } +} + +func performFullResyncHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("POST /full-resync started") + + waitList := genAll() + var errs string + for _, e := range waitList { + err := <-e + if err != nil { + errs += err.Error() + "\n" + } + } + + if errs != "" { + lastRegeneration = time.Now() + http.Error(w, errs, http.StatusInternalServerError) + } else { + http.Error(w, "done", http.StatusOK) + } + + log.Printf("POST /full-resync done") +} + +func appendGenQueue(gs fic.GenStruct) *fic.GenStruct { + if gs.Type == fic.GenTeam || gs.Type == fic.GenTeamIssues { + genQueue <- &gs + return &gs + } + // Append only if not already in queue inQueueMutex.RLock() if v, ok := inGenQueue[gs.Type]; !ok || !v { @@ -62,10 +105,12 @@ func appendGenQueue(gs fic.GenStruct) { inGenQueue[gs.Type] = true inQueueMutex.Unlock() - genQueue <- gs + genQueue <- &gs } else { inQueueMutex.RUnlock() } + + return &gs } func consumer() { @@ -98,6 +143,7 @@ func consumer() { if err != nil { log.Println(id, "[ERR] Unable to generate:", err) } + gs.End(err) } } @@ -263,18 +309,26 @@ func genThemesFile() error { return nil } -func genAll() { - appendGenQueue(fic.GenStruct{Type: fic.GenThemes}) - appendGenQueue(fic.GenStruct{Type: fic.GenTeams}) - appendGenQueue(fic.GenStruct{Type: fic.GenEvents}) - appendGenQueue(fic.GenStruct{Type: fic.GenPublic}) +func genAll() (waitList []chan error) { + waitList = append( + waitList, + appendGenQueue(fic.GenStruct{Type: fic.GenThemes}).GenEnded(), + appendGenQueue(fic.GenStruct{Type: fic.GenTeams}).GenEnded(), + appendGenQueue(fic.GenStruct{Type: fic.GenEvents}).GenEnded(), + appendGenQueue(fic.GenStruct{Type: fic.GenPublic}).GenEnded(), + ) if teams, err := fic.GetActiveTeams(); err != nil { log.Println("Team retrieval error: ", err) } else { for _, team := range teams { - appendGenQueue(fic.GenStruct{Type: fic.GenTeam, TeamId: team.Id}) - appendGenQueue(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id}) + waitList = append( + waitList, + appendGenQueue(fic.GenStruct{Type: fic.GenTeam, TeamId: team.Id}).GenEnded(), + appendGenQueue(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id}).GenEnded(), + ) } } + + return } diff --git a/generator/main.go b/generator/main.go index 5e3016c4..b457021b 100644 --- a/generator/main.go +++ b/generator/main.go @@ -29,7 +29,7 @@ func reloadSettings(config *settings.Settings) { 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.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness || fic.DiscountedFactor != config.DiscountedFactor { + if fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness || fic.DiscountedFactor != config.DiscountedFactor { fic.PartialValidation = config.PartialValidation fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo @@ -45,14 +45,17 @@ func reloadSettings(config *settings.Settings) { if !skipInitialGeneration { log.Println("Generating files...") go func() { - genAll() + waitList := genAll() + for _, e := range waitList { + <-e + } log.Println("Full generation done") }() + lastRegeneration = time.Now() } else { skipInitialGeneration = false log.Println("Regeneration skipped by option.") } - lastRegeneration = config.Generation } else { log.Println("No change found. Skipping regeneration.") } @@ -105,6 +108,8 @@ func main() { } http.HandleFunc("/enqueue", enqueueHandler) + http.HandleFunc("/perform", performHandler) + http.HandleFunc("/full", performFullResyncHandler) // Serve pages go func() { diff --git a/libfic/generation.go b/libfic/generation.go index 262c9d28..b78c3183 100644 --- a/libfic/generation.go +++ b/libfic/generation.go @@ -15,4 +15,20 @@ type GenStruct struct { Id string `json:"id"` Type GenerateType `json:"type"` TeamId int64 `json:"team_id,omitempty"` + ended chan error +} + +func (gs *GenStruct) GenEnded() chan error { + gs.ended = make(chan error, 1) + return gs.ended +} + +func (gs *GenStruct) GetEnded() chan error { + return gs.ended +} + +func (gs *GenStruct) End(err error) { + if gs.ended != nil { + gs.ended <- err + } } diff --git a/settings/settings.go b/settings/settings.go index 4d83654e..f69f09a0 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -29,8 +29,6 @@ type Settings struct { Start time.Time `json:"start"` // End is the expected end time (if empty their is no end-date). End *time.Time `json:"end,omitempty"` - // 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"` @@ -130,17 +128,6 @@ func SaveSettings(path string, s interface{}) error { } } -// 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