From 9a2e47b23a6d460acfce9b1b77e6f9fb06ca1b75 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 17 May 2020 00:31:38 +0100 Subject: [PATCH] Refactor Cron and merge dashboard tasks (#10745) * Refactor Cron and merge dashboard tasks * Merge Cron and Dashboard tasks * Make every cron task report a system notice on completion * Refactor the creation of these tasks * Ensure that execution counts of tasks is correct * Allow cron tasks to be started from the cron page * golangci-lint fixes * Enforce that only one task with the same name can be registered Signed-off-by: Andrew Thornton * fix name check Signed-off-by: Andrew Thornton * as per @guillep2k * as per @lafriks Signed-off-by: Andrew Thornton * Add git.CommandContext variants Signed-off-by: Andrew Thornton Co-authored-by: Lauris BH Co-authored-by: Lunny Xiao Co-authored-by: techknowlogick --- integrations/auth_ldap_test.go | 4 +- models/admin.go | 15 ++- models/branches.go | 5 +- models/error.go | 22 ++++ models/repo.go | 65 +++++++----- models/user.go | 51 +++++---- modules/auth/admin.go | 2 +- modules/cron/cron.go | 179 +++++++++++--------------------- modules/cron/setting.go | 72 +++++++++++++ modules/cron/tasks.go | 166 +++++++++++++++++++++++++++++ modules/cron/tasks_basic.go | 118 +++++++++++++++++++++ modules/cron/tasks_extended.go | 119 +++++++++++++++++++++ modules/git/command.go | 14 ++- modules/git/git.go | 4 +- modules/migrations/update.go | 10 +- modules/repository/check.go | 102 +++++++++++------- modules/repository/hooks.go | 9 +- modules/setting/cron.go | 129 +---------------------- modules/setting/setting.go | 1 - options/locale/locale_en-US.ini | 36 ++++--- routers/admin/admin.go | 65 ++---------- routers/init.go | 35 +++++++ services/mirror/mirror.go | 9 +- templates/admin/dashboard.tmpl | 27 ++--- templates/admin/monitor.tmpl | 45 ++++---- 25 files changed, 851 insertions(+), 453 deletions(-) create mode 100644 modules/cron/setting.go create mode 100644 modules/cron/tasks.go create mode 100644 modules/cron/tasks_basic.go create mode 100644 modules/cron/tasks_extended.go diff --git a/integrations/auth_ldap_test.go b/integrations/auth_ldap_test.go index 6c6147f20..520a611ea 100644 --- a/integrations/auth_ldap_test.go +++ b/integrations/auth_ldap_test.go @@ -151,7 +151,7 @@ func TestLDAPUserSync(t *testing.T) { } defer prepareTestEnv(t)() addAuthSourceLDAP(t, "") - models.SyncExternalUsers(context.Background()) + models.SyncExternalUsers(context.Background(), true) session := loginUser(t, "user1") // Check if users exists @@ -216,7 +216,7 @@ func TestLDAPUserSSHKeySync(t *testing.T) { defer prepareTestEnv(t)() addAuthSourceLDAP(t, "sshPublicKey") - models.SyncExternalUsers(context.Background()) + models.SyncExternalUsers(context.Background(), true) // Check if users has SSH keys synced for _, u := range gitLDAPUsers { diff --git a/models/admin.go b/models/admin.go index 271c5307a..01f0ba348 100644 --- a/models/admin.go +++ b/models/admin.go @@ -20,6 +20,8 @@ type NoticeType int const ( //NoticeRepository type NoticeRepository NoticeType = iota + 1 + // NoticeTask type + NoticeTask ) // Notice represents a system notice for admin. @@ -36,11 +38,14 @@ func (n *Notice) TrStr() string { } // CreateNotice creates new system notice. -func CreateNotice(tp NoticeType, desc string) error { - return createNotice(x, tp, desc) +func CreateNotice(tp NoticeType, desc string, args ...interface{}) error { + return createNotice(x, tp, desc, args...) } -func createNotice(e Engine, tp NoticeType, desc string) error { +func createNotice(e Engine, tp NoticeType, desc string, args ...interface{}) error { + if len(args) > 0 { + desc = fmt.Sprintf(desc, args...) + } n := &Notice{ Type: tp, Description: desc, @@ -50,8 +55,8 @@ func createNotice(e Engine, tp NoticeType, desc string) error { } // CreateRepositoryNotice creates new system notice with type NoticeRepository. -func CreateRepositoryNotice(desc string) error { - return createNotice(x, NoticeRepository, desc) +func CreateRepositoryNotice(desc string, args ...interface{}) error { + return createNotice(x, NoticeRepository, desc, args...) } // RemoveAllWithNotice removes all directories in given path and diff --git a/models/branches.go b/models/branches.go index e6b8d61a7..688e2d7f5 100644 --- a/models/branches.go +++ b/models/branches.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -561,11 +560,11 @@ func RemoveDeletedBranch(repoID int64, branch string) error { } // RemoveOldDeletedBranches removes old deleted branches -func RemoveOldDeletedBranches(ctx context.Context) { +func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { // Nothing to do for shutdown or terminate log.Trace("Doing: DeletedBranchesCleanup") - deleteBefore := time.Now().Add(-setting.Cron.DeletedBranchesCleanup.OlderThan) + deleteBefore := time.Now().Add(-olderThan) _, err := x.Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch)) if err != nil { log.Error("DeletedBranchesCleanup: %v", err) diff --git a/models/error.go b/models/error.go index 7370bd157..3b05a7152 100644 --- a/models/error.go +++ b/models/error.go @@ -85,6 +85,28 @@ func (err ErrSSHDisabled) Error() string { return "SSH is disabled" } +// ErrCancelled represents an error due to context cancellation +type ErrCancelled struct { + Message string +} + +// IsErrCancelled checks if an error is a ErrCancelled. +func IsErrCancelled(err error) bool { + _, ok := err.(ErrCancelled) + return ok +} + +func (err ErrCancelled) Error() string { + return "Cancelled: " + err.Message +} + +// ErrCancelledf returns an ErrCancelled for the provided format and args +func ErrCancelledf(format string, args ...interface{}) error { + return ErrCancelled{ + fmt.Sprintf(format, args...), + } +} + // ____ ___ // | | \______ ___________ // | | / ___// __ \_ __ \ diff --git a/models/repo.go b/models/repo.go index 875c9ad1c..f79740e74 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1853,35 +1853,44 @@ func GetPrivateRepositoryCount(u *User) (int64, error) { } // DeleteRepositoryArchives deletes all repositories' archives. -func DeleteRepositoryArchives() error { +func DeleteRepositoryArchives(ctx context.Context) error { return x. Where("id > 0"). Iterate(new(Repository), func(idx int, bean interface{}) error { repo := bean.(*Repository) + select { + case <-ctx.Done(): + return ErrCancelledf("before deleting repository archives for %s", repo.FullName()) + default: + } return os.RemoveAll(filepath.Join(repo.RepoPath(), "archives")) }) } // DeleteOldRepositoryArchives deletes old repository archives. -func DeleteOldRepositoryArchives(ctx context.Context) { +func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) error { log.Trace("Doing: ArchiveCleanup") if err := x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { - return deleteOldRepositoryArchives(ctx, idx, bean) + return deleteOldRepositoryArchives(ctx, olderThan, idx, bean) }); err != nil { - log.Error("ArchiveClean: %v", err) + log.Trace("Error: ArchiveClean: %v", err) + return err } + + log.Trace("Finished: ArchiveCleanup") + return nil } -func deleteOldRepositoryArchives(ctx context.Context, idx int, bean interface{}) error { +func deleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration, idx int, bean interface{}) error { repo := bean.(*Repository) basePath := filepath.Join(repo.RepoPath(), "archives") for _, ty := range []string{"zip", "targz"} { select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown:\nin delete of old repository archives %v\nat delete file %s", repo, ty) + return ErrCancelledf("before deleting old repository archives with filetype %s for %s", ty, repo.FullName()) default: } @@ -1904,12 +1913,12 @@ func deleteOldRepositoryArchives(ctx context.Context, idx int, bean interface{}) return err } - minimumOldestTime := time.Now().Add(-setting.Cron.ArchiveCleanup.OlderThan) + minimumOldestTime := time.Now().Add(-olderThan) for _, info := range files { if info.ModTime().Before(minimumOldestTime) && !info.IsDir() { select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown:\nin delete of old repository archives %v\nat delete file %s - %s", repo, ty, info.Name()) + return ErrCancelledf("before deleting old repository archive file %s with filetype %s for %s", info.Name(), ty, repo.FullName()) default: } toDelete := filepath.Join(path, info.Name()) @@ -1936,13 +1945,13 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { return } for _, result := range results { + id := com.StrTo(result["id"]).MustInt64() select { case <-ctx.Done(): - log.Warn("CheckRepoStats: Aborting due to shutdown") + log.Warn("CheckRepoStats: Cancelled before checking %s for Repo[%d]", checker.desc, id) return default: } - id := com.StrTo(result["id"]).MustInt64() log.Trace("Updating %s: %d", checker.desc, id) _, err = x.Exec(checker.correctSQL, id, id) if err != nil { @@ -1952,7 +1961,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { } // CheckRepoStats checks the repository stats -func CheckRepoStats(ctx context.Context) { +func CheckRepoStats(ctx context.Context) error { log.Trace("Doing: CheckRepoStats") checkers := []*repoChecker{ @@ -1987,13 +1996,13 @@ func CheckRepoStats(ctx context.Context) { "issue count 'num_comments'", }, } - for i := range checkers { + for _, checker := range checkers { select { case <-ctx.Done(): - log.Warn("CheckRepoStats: Aborting due to shutdown") - return + log.Warn("CheckRepoStats: Cancelled before %s", checker.desc) + return ErrCancelledf("before checking %s", checker.desc) default: - repoStatsCheck(ctx, checkers[i]) + repoStatsCheck(ctx, checker) } } @@ -2004,13 +2013,13 @@ func CheckRepoStats(ctx context.Context) { log.Error("Select %s: %v", desc, err) } else { for _, result := range results { + id := com.StrTo(result["id"]).MustInt64() select { case <-ctx.Done(): - log.Warn("CheckRepoStats: Aborting due to shutdown") - return + log.Warn("CheckRepoStats: Cancelled during %s for repo ID %d", desc, id) + return ErrCancelledf("during %s for repo ID %d", desc, id) default: } - id := com.StrTo(result["id"]).MustInt64() log.Trace("Updating %s: %d", desc, id) _, err = x.Exec("UPDATE `repository` SET num_closed_issues=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, false, id) if err != nil { @@ -2027,13 +2036,13 @@ func CheckRepoStats(ctx context.Context) { log.Error("Select %s: %v", desc, err) } else { for _, result := range results { + id := com.StrTo(result["id"]).MustInt64() select { case <-ctx.Done(): - log.Warn("CheckRepoStats: Aborting due to shutdown") - return + log.Warn("CheckRepoStats: Cancelled") + return ErrCancelledf("during %s for repo ID %d", desc, id) default: } - id := com.StrTo(result["id"]).MustInt64() log.Trace("Updating %s: %d", desc, id) _, err = x.Exec("UPDATE `repository` SET num_closed_pulls=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, true, id) if err != nil { @@ -2050,13 +2059,13 @@ func CheckRepoStats(ctx context.Context) { log.Error("Select repository count 'num_forks': %v", err) } else { for _, result := range results { + id := com.StrTo(result["id"]).MustInt64() select { case <-ctx.Done(): - log.Warn("CheckRepoStats: Aborting due to shutdown") - return + log.Warn("CheckRepoStats: Cancelled") + return ErrCancelledf("during %s for repo ID %d", desc, id) default: } - id := com.StrTo(result["id"]).MustInt64() log.Trace("Updating repository count 'num_forks': %d", id) repo, err := GetRepositoryByID(id) @@ -2079,6 +2088,7 @@ func CheckRepoStats(ctx context.Context) { } } // ***** END: Repository.NumForks ***** + return nil } // SetArchiveRepoState sets if a repo is archived @@ -2189,12 +2199,17 @@ func (repo *Repository) generateRandomAvatar(e Engine) error { } // RemoveRandomAvatars removes the randomly generated avatars that were created for repositories -func RemoveRandomAvatars() error { +func RemoveRandomAvatars(ctx context.Context) error { return x. Where("id > 0").BufferSize(setting.Database.IterateBufferSize). Iterate(new(Repository), func(idx int, bean interface{}) error { repository := bean.(*Repository) + select { + case <-ctx.Done(): + return ErrCancelledf("before random avatars removed for %s", repository.FullName()) + default: + } stringifiedID := strconv.FormatInt(repository.ID, 10) if repository.Avatar == stringifiedID { return repository.DeleteAvatar() diff --git a/models/user.go b/models/user.go index 01cce2506..8875840db 100644 --- a/models/user.go +++ b/models/user.go @@ -1321,16 +1321,30 @@ func DeleteUser(u *User) (err error) { return sess.Commit() } -// DeleteInactivateUsers deletes all inactivate users and email addresses. -func DeleteInactivateUsers() (err error) { +// DeleteInactiveUsers deletes all inactive users and email addresses. +func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) (err error) { users := make([]*User, 0, 10) - if err = x. - Where("is_active = ?", false). - Find(&users); err != nil { - return fmt.Errorf("get all inactive users: %v", err) + if olderThan > 0 { + if err = x. + Where("is_active = ? and created_unix < ?", false, time.Now().Add(-olderThan).Unix()). + Find(&users); err != nil { + return fmt.Errorf("get all inactive users: %v", err) + } + } else { + if err = x. + Where("is_active = ?", false). + Find(&users); err != nil { + return fmt.Errorf("get all inactive users: %v", err) + } + } // FIXME: should only update authorized_keys file once after all deletions. for _, u := range users { + select { + case <-ctx.Done(): + return ErrCancelledf("Before delete inactive user %s", u.Name) + default: + } if err = DeleteUser(u); err != nil { // Ignore users that were set inactive by admin. if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) { @@ -1814,25 +1828,23 @@ func synchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []str } // SyncExternalUsers is used to synchronize users with external authorization source -func SyncExternalUsers(ctx context.Context) { +func SyncExternalUsers(ctx context.Context, updateExisting bool) error { log.Trace("Doing: SyncExternalUsers") ls, err := LoginSources() if err != nil { log.Error("SyncExternalUsers: %v", err) - return + return err } - updateExisting := setting.Cron.SyncExternalUsers.UpdateExisting - for _, s := range ls { if !s.IsActived || !s.IsSyncEnabled { continue } select { case <-ctx.Done(): - log.Warn("SyncExternalUsers: Aborted due to shutdown before update of %s", s.Name) - return + log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name) + return ErrCancelledf("Before update of %s", s.Name) default: } @@ -1850,12 +1862,12 @@ func SyncExternalUsers(ctx context.Context) { Find(&users) if err != nil { log.Error("SyncExternalUsers: %v", err) - return + return err } select { case <-ctx.Done(): - log.Warn("SyncExternalUsers: Aborted due to shutdown before update of %s", s.Name) - return + log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name) + return ErrCancelledf("Before update of %s", s.Name) default: } @@ -1877,7 +1889,7 @@ func SyncExternalUsers(ctx context.Context) { for _, su := range sr { select { case <-ctx.Done(): - log.Warn("SyncExternalUsers: Aborted due to shutdown at update of %s before completed update of users", s.Name) + log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", s.Name) // Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed if sshKeysNeedUpdate { err = RewriteAllPublicKeys() @@ -1885,7 +1897,7 @@ func SyncExternalUsers(ctx context.Context) { log.Error("RewriteAllPublicKeys: %v", err) } } - return + return ErrCancelledf("During update of %s before completed update of users", s.Name) default: } if len(su.Username) == 0 { @@ -1980,8 +1992,8 @@ func SyncExternalUsers(ctx context.Context) { select { case <-ctx.Done(): - log.Warn("SyncExternalUsers: Aborted due to shutdown at update of %s before delete users", s.Name) - return + log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", s.Name) + return ErrCancelledf("During update of %s before delete users", s.Name) default: } @@ -2008,4 +2020,5 @@ func SyncExternalUsers(ctx context.Context) { } } } + return nil } diff --git a/modules/auth/admin.go b/modules/auth/admin.go index a77268068..af7197d2a 100644 --- a/modules/auth/admin.go +++ b/modules/auth/admin.go @@ -51,7 +51,7 @@ func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) // AdminDashboardForm form for admin dashboard operations type AdminDashboardForm struct { - Op int `binding:"required"` + Op string `binding:"required"` } // Validate validates form fields diff --git a/modules/cron/cron.go b/modules/cron/cron.go index 692642e4c..ae309bd86 100644 --- a/modules/cron/cron.go +++ b/modules/cron/cron.go @@ -9,143 +9,86 @@ import ( "context" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/migrations" - repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sync" - mirror_service "code.gitea.io/gitea/services/mirror" "github.com/gogs/cron" ) -const ( - mirrorUpdate = "mirror_update" - gitFsck = "git_fsck" - checkRepos = "check_repos" - archiveCleanup = "archive_cleanup" - syncExternalUsers = "sync_external_users" - deletedBranchesCleanup = "deleted_branches_cleanup" - updateMigrationPosterID = "update_migration_post_id" -) - var c = cron.New() // Prevent duplicate running tasks. var taskStatusTable = sync.NewStatusTable() -// Func defines a cron function body -type Func func() - -// WithUnique wrap a cron func with an unique running check -func WithUnique(name string, body func(context.Context)) Func { - return func() { - if !taskStatusTable.StartIfNotRunning(name) { - return - } - defer taskStatusTable.Stop(name) - graceful.GetManager().RunWithShutdownContext(body) - } -} - // NewContext begins cron tasks // Each cron task is run within the shutdown context as a running server // AtShutdown the cron server is stopped func NewContext() { - var ( - entry *cron.Entry - err error - ) - if setting.Cron.UpdateMirror.Enabled { - entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, WithUnique(mirrorUpdate, mirror_service.Update)) - if err != nil { - log.Fatal("Cron[Update mirrors]: %v", err) - } - if setting.Cron.UpdateMirror.RunAtStart { - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(mirrorUpdate, mirror_service.Update)() - } - } - if setting.Cron.RepoHealthCheck.Enabled { - entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, WithUnique(gitFsck, func(ctx context.Context) { - if err := repo_module.GitFsck(ctx); err != nil { - log.Error("GitFsck: %s", err) - } - })) - if err != nil { - log.Fatal("Cron[Repository health check]: %v", err) - } - if setting.Cron.RepoHealthCheck.RunAtStart { - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(gitFsck, func(ctx context.Context) { - if err := repo_module.GitFsck(ctx); err != nil { - log.Error("GitFsck: %s", err) - } - })() - } - } - if setting.Cron.CheckRepoStats.Enabled { - entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, WithUnique(checkRepos, models.CheckRepoStats)) - if err != nil { - log.Fatal("Cron[Check repository statistics]: %v", err) - } - if setting.Cron.CheckRepoStats.RunAtStart { - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(checkRepos, models.CheckRepoStats)() - } - } - if setting.Cron.ArchiveCleanup.Enabled { - entry, err = c.AddFunc("Clean up old repository archives", setting.Cron.ArchiveCleanup.Schedule, WithUnique(archiveCleanup, models.DeleteOldRepositoryArchives)) - if err != nil { - log.Fatal("Cron[Clean up old repository archives]: %v", err) - } - if setting.Cron.ArchiveCleanup.RunAtStart { - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(archiveCleanup, models.DeleteOldRepositoryArchives)() - } - } - if setting.Cron.SyncExternalUsers.Enabled { - entry, err = c.AddFunc("Synchronize external users", setting.Cron.SyncExternalUsers.Schedule, WithUnique(syncExternalUsers, models.SyncExternalUsers)) - if err != nil { - log.Fatal("Cron[Synchronize external users]: %v", err) - } - if setting.Cron.SyncExternalUsers.RunAtStart { - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(syncExternalUsers, models.SyncExternalUsers)() - } - } - if setting.Cron.DeletedBranchesCleanup.Enabled { - entry, err = c.AddFunc("Remove old deleted branches", setting.Cron.DeletedBranchesCleanup.Schedule, WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)) - if err != nil { - log.Fatal("Cron[Remove old deleted branches]: %v", err) - } - if setting.Cron.DeletedBranchesCleanup.RunAtStart { - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)() - } - } + initBasicTasks() + initExtendedTasks() - entry, err = c.AddFunc("Update migrated repositories' issues and comments' posterid", setting.Cron.UpdateMigrationPosterID.Schedule, WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)) - if err != nil { - log.Fatal("Cron[Update migrated repositories]: %v", err) + lock.Lock() + for _, task := range tasks { + if task.IsEnabled() && task.DoRunAtStart() { + go task.Run() + } } - entry.Prev = time.Now() - entry.ExecTimes++ - go WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)() c.Start() - graceful.GetManager().RunAtShutdown(context.Background(), c.Stop) + started = true + lock.Unlock() + graceful.GetManager().RunAtShutdown(context.Background(), func() { + c.Stop() + lock.Lock() + started = false + lock.Unlock() + }) + } -// ListTasks returns all running cron tasks. -func ListTasks() []*cron.Entry { - return c.Entries() +// TaskTableRow represents a task row in the tasks table +type TaskTableRow struct { + Name string + Spec string + Next time.Time + Prev time.Time + ExecTimes int64 +} + +// TaskTable represents a table of tasks +type TaskTable []*TaskTableRow + +// ListTasks returns all running cron tasks. +func ListTasks() TaskTable { + entries := c.Entries() + eMap := map[string]*cron.Entry{} + for _, e := range entries { + eMap[e.Description] = e + } + lock.Lock() + defer lock.Unlock() + tTable := make([]*TaskTableRow, 0, len(tasks)) + for _, task := range tasks { + spec := "-" + var ( + next time.Time + prev time.Time + ) + if e, ok := eMap[task.Name]; ok { + spec = e.Spec + next = e.Next + prev = e.Prev + } + task.lock.Lock() + tTable = append(tTable, &TaskTableRow{ + Name: task.Name, + Spec: spec, + Next: next, + Prev: prev, + ExecTimes: task.ExecTimes, + }) + task.lock.Unlock() + } + + return tTable } diff --git a/modules/cron/setting.go b/modules/cron/setting.go new file mode 100644 index 000000000..dd93d0398 --- /dev/null +++ b/modules/cron/setting.go @@ -0,0 +1,72 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cron + +import ( + "time" + + "code.gitea.io/gitea/models" + "github.com/unknwon/i18n" +) + +// Config represents a basic configuration interface that cron task +type Config interface { + IsEnabled() bool + DoRunAtStart() bool + GetSchedule() string + FormatMessage(name, status string, doer *models.User, args ...interface{}) string +} + +// BaseConfig represents the basic config for a Cron task +type BaseConfig struct { + Enabled bool + RunAtStart bool + Schedule string +} + +// OlderThanConfig represents a cron task with OlderThan setting +type OlderThanConfig struct { + BaseConfig + OlderThan time.Duration +} + +// UpdateExistingConfig represents a cron task with UpdateExisting setting +type UpdateExistingConfig struct { + BaseConfig + UpdateExisting bool +} + +// GetSchedule returns the schedule for the base config +func (b *BaseConfig) GetSchedule() string { + return b.Schedule +} + +// IsEnabled returns the enabled status for the config +func (b *BaseConfig) IsEnabled() bool { + return b.Enabled +} + +// DoRunAtStart returns whether the task should be run at the start +func (b *BaseConfig) DoRunAtStart() bool { + return b.RunAtStart +} + +// FormatMessage returns a message for the task +func (b *BaseConfig) FormatMessage(name, status string, doer *models.User, args ...interface{}) string { + realArgs := make([]interface{}, 0, len(args)+2) + realArgs = append(realArgs, i18n.Tr("en-US", "admin.dashboard."+name)) + if doer == nil { + realArgs = append(realArgs, "(Cron)") + } else { + realArgs = append(realArgs, doer.Name) + } + if len(args) > 0 { + realArgs = append(realArgs, args...) + } + if doer == nil || (doer.ID == -1 && doer.Name == "(Cron)") { + return i18n.Tr("en-US", "admin.dashboard.cron."+status, realArgs...) + } + return i18n.Tr("en-US", "admin.dashboard.task."+status, realArgs...) +} diff --git a/modules/cron/tasks.go b/modules/cron/tasks.go new file mode 100644 index 000000000..a97326bd0 --- /dev/null +++ b/modules/cron/tasks.go @@ -0,0 +1,166 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cron + +import ( + "context" + "fmt" + "reflect" + "sync" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" +) + +var lock = sync.Mutex{} +var started = false +var tasks = []*Task{} +var tasksMap = map[string]*Task{} + +// Task represents a Cron task +type Task struct { + lock sync.Mutex + Name string + config Config + fun func(context.Context, *models.User, Config) error + ExecTimes int64 +} + +// DoRunAtStart returns if this task should run at the start +func (t *Task) DoRunAtStart() bool { + return t.config.DoRunAtStart() +} + +// IsEnabled returns if this task is enabled as cron task +func (t *Task) IsEnabled() bool { + return t.config.IsEnabled() +} + +// GetConfig will return a copy of the task's config +func (t *Task) GetConfig() Config { + if reflect.TypeOf(t.config).Kind() == reflect.Ptr { + // Pointer: + return reflect.New(reflect.ValueOf(t.config).Elem().Type()).Interface().(Config) + } + // Not pointer: + return reflect.New(reflect.TypeOf(t.config)).Elem().Interface().(Config) +} + +// Run will run the task incrementing the cron counter with no user defined +func (t *Task) Run() { + t.RunWithUser(&models.User{ + ID: -1, + Name: "(Cron)", + LowerName: "(cron)", + }, t.config) +} + +// RunWithUser will run the task incrementing the cron counter at the time with User +func (t *Task) RunWithUser(doer *models.User, config Config) { + if !taskStatusTable.StartIfNotRunning(t.Name) { + return + } + t.lock.Lock() + if config == nil { + config = t.config + } + t.ExecTimes++ + t.lock.Unlock() + defer func() { + taskStatusTable.Stop(t.Name) + if err := recover(); err != nil { + // Recover a panic within the + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr) + } + }() + graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) { + ctx, cancel := context.WithCancel(baseCtx) + defer cancel() + pm := process.GetManager() + pid := pm.Add(config.FormatMessage(t.Name, "process", doer), cancel) + defer pm.Remove(pid) + if err := t.fun(ctx, doer, config); err != nil { + if models.IsErrCancelled(err) { + message := err.(models.ErrCancelled).Message + if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil { + log.Error("CreateNotice: %v", err) + } + return + } + if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil { + log.Error("CreateNotice: %v", err) + } + return + } + if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); err != nil { + log.Error("CreateNotice: %v", err) + } + }) +} + +// GetTask gets the named task +func GetTask(name string) *Task { + lock.Lock() + defer lock.Unlock() + log.Info("Getting %s in %v", name, tasksMap[name]) + + return tasksMap[name] +} + +// RegisterTask allows a task to be registered with the cron service +func RegisterTask(name string, config Config, fun func(context.Context, *models.User, Config) error) error { + log.Debug("Registering task: %s", name) + _, err := setting.GetCronSettings(name, config) + if err != nil { + log.Error("Unable to register cron task with name: %s Error: %v", name, err) + return err + } + + task := &Task{ + Name: name, + config: config, + fun: fun, + } + lock.Lock() + locked := true + defer func() { + if locked { + lock.Unlock() + } + }() + if _, has := tasksMap[task.Name]; has { + log.Error("A task with this name: %s has already been registered", name) + return fmt.Errorf("duplicate task with name: %s", task.Name) + } + + if config.IsEnabled() { + // We cannot use the entry return as there is no way to lock it + if _, err = c.AddJob(name, config.GetSchedule(), task); err != nil { + log.Error("Unable to register cron task with name: %s Error: %v", name, err) + return err + } + } + + tasks = append(tasks, task) + tasksMap[task.Name] = task + if started && config.IsEnabled() && config.DoRunAtStart() { + lock.Unlock() + locked = false + task.Run() + } + + return nil +} + +// RegisterTaskFatal will register a task but if there is an error log.Fatal +func RegisterTaskFatal(name string, config Config, fun func(context.Context, *models.User, Config) error) { + if err := RegisterTask(name, config, fun); err != nil { + log.Fatal("Unable to register cron task %s Error: %v", name, err) + } +} diff --git a/modules/cron/tasks_basic.go b/modules/cron/tasks_basic.go new file mode 100644 index 000000000..438c4a500 --- /dev/null +++ b/modules/cron/tasks_basic.go @@ -0,0 +1,118 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cron + +import ( + "context" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/migrations" + repository_service "code.gitea.io/gitea/modules/repository" + mirror_service "code.gitea.io/gitea/services/mirror" +) + +func registerUpdateMirrorTask() { + RegisterTaskFatal("update_mirrors", &BaseConfig{ + Enabled: true, + RunAtStart: false, + Schedule: "@every 10m", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return mirror_service.Update(ctx) + }) +} + +func registerRepoHealthCheck() { + type RepoHealthCheckConfig struct { + BaseConfig + Timeout time.Duration + Args []string `delim:" "` + } + RegisterTaskFatal("repo_health_check", &RepoHealthCheckConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: false, + Schedule: "@every 24h", + }, + Timeout: 60 * time.Second, + Args: []string{}, + }, func(ctx context.Context, _ *models.User, config Config) error { + rhcConfig := config.(*RepoHealthCheckConfig) + return repository_service.GitFsck(ctx, rhcConfig.Timeout, rhcConfig.Args) + }) +} + +func registerCheckRepoStats() { + RegisterTaskFatal("check_repo_stats", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 24h", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return models.CheckRepoStats(ctx) + }) +} + +func registerArchiveCleanup() { + RegisterTaskFatal("archive_cleanup", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 24h", + }, + OlderThan: 24 * time.Hour, + }, func(ctx context.Context, _ *models.User, config Config) error { + acConfig := config.(*OlderThanConfig) + return models.DeleteOldRepositoryArchives(ctx, acConfig.OlderThan) + }) +} + +func registerSyncExternalUsers() { + RegisterTaskFatal("sync_external_users", &UpdateExistingConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: false, + Schedule: "@every 24h", + }, + UpdateExisting: true, + }, func(ctx context.Context, _ *models.User, config Config) error { + realConfig := config.(*UpdateExistingConfig) + return models.SyncExternalUsers(ctx, realConfig.UpdateExisting) + }) +} + +func registerDeletedBranchesCleanup() { + RegisterTaskFatal("deleted_branches_cleanup", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 24h", + }, + OlderThan: 24 * time.Hour, + }, func(ctx context.Context, _ *models.User, config Config) error { + realConfig := config.(*OlderThanConfig) + models.RemoveOldDeletedBranches(ctx, realConfig.OlderThan) + return nil + }) +} + +func registerUpdateMigrationPosterID() { + RegisterTaskFatal("update_migration_poster_id", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 24h", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return migrations.UpdateMigrationPosterID(ctx) + }) +} + +func initBasicTasks() { + registerUpdateMirrorTask() + registerRepoHealthCheck() + registerCheckRepoStats() + registerArchiveCleanup() + registerSyncExternalUsers() + registerDeletedBranchesCleanup() + registerUpdateMigrationPosterID() +} diff --git a/modules/cron/tasks_extended.go b/modules/cron/tasks_extended.go new file mode 100644 index 000000000..fa2d6e0c3 --- /dev/null +++ b/modules/cron/tasks_extended.go @@ -0,0 +1,119 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cron + +import ( + "context" + "time" + + "code.gitea.io/gitea/models" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" +) + +func registerDeleteInactiveUsers() { + RegisterTaskFatal("delete_inactive_accounts", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@annually", + }, + OlderThan: 0 * time.Second, + }, func(ctx context.Context, _ *models.User, config Config) error { + olderThanConfig := config.(*OlderThanConfig) + return models.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan) + }) +} + +func registerDeleteRepositoryArchives() { + RegisterTaskFatal("delete_repo_archives", &BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@annually", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return models.DeleteRepositoryArchives(ctx) + }) +} + +func registerGarbageCollectRepositories() { + type RepoHealthCheckConfig struct { + BaseConfig + Timeout time.Duration + Args []string `delim:" "` + } + RegisterTaskFatal("git_gc_repos", &RepoHealthCheckConfig{ + BaseConfig: BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@every 72h", + }, + Timeout: time.Duration(setting.Git.Timeout.GC) * time.Second, + Args: setting.Git.GCArgs, + }, func(ctx context.Context, _ *models.User, config Config) error { + rhcConfig := config.(*RepoHealthCheckConfig) + return repo_module.GitGcRepos(ctx, rhcConfig.Timeout, rhcConfig.Args...) + }) +} + +func registerRewriteAllPublicKeys() { + RegisterTaskFatal("resync_all_sshkeys", &BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@every 72h", + }, func(_ context.Context, _ *models.User, _ Config) error { + return models.RewriteAllPublicKeys() + }) +} + +func registerRepositoryUpdateHook() { + RegisterTaskFatal("resync_all_hooks", &BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@every 72h", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return repo_module.SyncRepositoryHooks(ctx) + }) +} + +func registerReinitMissingRepositories() { + RegisterTaskFatal("reinit_missing_repos", &BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@every 72h", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return repo_module.ReinitMissingRepositories(ctx) + }) +} + +func registerDeleteMissingRepositories() { + RegisterTaskFatal("delete_missing_repos", &BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@every 72h", + }, func(ctx context.Context, user *models.User, _ Config) error { + return repo_module.DeleteMissingRepositories(ctx, user) + }) +} + +func registerRemoveRandomAvatars() { + RegisterTaskFatal("delete_generated_repository_avatars", &BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@every 72h", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return models.RemoveRandomAvatars(ctx) + }) +} + +func initExtendedTasks() { + registerDeleteInactiveUsers() + registerDeleteRepositoryArchives() + registerGarbageCollectRepositories() + registerRewriteAllPublicKeys() + registerRepositoryUpdateHook() + registerReinitMissingRepositories() + registerDeleteMissingRepositories() + registerRemoveRandomAvatars() +} diff --git a/modules/git/command.go b/modules/git/command.go index 6c931790c..14fab4ef3 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -45,22 +45,32 @@ func (c *Command) String() string { // NewCommand creates and returns a new Git Command based on given command and arguments. func NewCommand(args ...string) *Command { + return NewCommandContext(DefaultContext, args...) +} + +// NewCommandContext creates and returns a new Git Command based on given command and arguments. +func NewCommandContext(ctx context.Context, args ...string) *Command { // Make an explicit copy of GlobalCommandArgs, otherwise append might overwrite it cargs := make([]string, len(GlobalCommandArgs)) copy(cargs, GlobalCommandArgs) return &Command{ name: GitExecutable, args: append(cargs, args...), - parentContext: DefaultContext, + parentContext: ctx, } } // NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args func NewCommandNoGlobals(args ...string) *Command { + return NewCommandContextNoGlobals(DefaultContext, args...) +} + +// NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args +func NewCommandContextNoGlobals(ctx context.Context, args ...string) *Command { return &Command{ name: GitExecutable, args: args, - parentContext: DefaultContext, + parentContext: ctx, } } diff --git a/modules/git/git.go b/modules/git/git.go index d5caaa091..7f718511f 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -150,11 +150,11 @@ func Init(ctx context.Context) error { } // Fsck verifies the connectivity and validity of the objects in the database -func Fsck(repoPath string, timeout time.Duration, args ...string) error { +func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...string) error { // Make sure timeout makes sense. if timeout <= 0 { timeout = -1 } - _, err := NewCommand("fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath) + _, err := NewCommandContext(ctx, "fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath) return err } diff --git a/modules/migrations/update.go b/modules/migrations/update.go index 3d0962657..e7a57ceca 100644 --- a/modules/migrations/update.go +++ b/modules/migrations/update.go @@ -13,17 +13,19 @@ import ( ) // UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID -func UpdateMigrationPosterID(ctx context.Context) { +func UpdateMigrationPosterID(ctx context.Context) error { for _, gitService := range structs.SupportedFullGitService { select { case <-ctx.Done(): - log.Warn("UpdateMigrationPosterID aborted due to shutdown before %s", gitService.Name()) + log.Warn("UpdateMigrationPosterID aborted before %s", gitService.Name()) + return models.ErrCancelledf("during UpdateMigrationPosterID before %s", gitService.Name()) default: } if err := updateMigrationPosterIDByGitService(ctx, gitService); err != nil { log.Error("updateMigrationPosterIDByGitService failed: %v", err) } } + return nil } func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServiceType) error { @@ -37,7 +39,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ for { select { case <-ctx.Done(): - log.Warn("UpdateMigrationPosterIDByGitService(%s) aborted due to shutdown", tp.Name()) + log.Warn("UpdateMigrationPosterIDByGitService(%s) cancelled", tp.Name()) return nil default: } @@ -54,7 +56,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ for _, user := range users { select { case <-ctx.Done(): - log.Warn("UpdateMigrationPosterIDByGitService(%s) aborted due to shutdown", tp.Name()) + log.Warn("UpdateMigrationPosterIDByGitService(%s) cancelled", tp.Name()) return nil default: } diff --git a/modules/repository/check.go b/modules/repository/check.go index fcaf76308..90186d6a2 100644 --- a/modules/repository/check.go +++ b/modules/repository/check.go @@ -7,19 +7,19 @@ package repository import ( "context" "fmt" + "strings" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "github.com/unknwon/com" "xorm.io/builder" ) // GitFsck calls 'git fsck' to check repository health. -func GitFsck(ctx context.Context) error { +func GitFsck(ctx context.Context, timeout time.Duration, args []string) error { log.Trace("Doing: GitFsck") if err := models.Iterate( @@ -27,24 +27,24 @@ func GitFsck(ctx context.Context) error { new(models.Repository), builder.Expr("id>0 AND is_fsck_enabled=?", true), func(idx int, bean interface{}) error { + repo := bean.(*models.Repository) select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown") + return models.ErrCancelledf("before fsck of %s", repo.FullName()) default: } - repo := bean.(*models.Repository) + log.Trace("Running health check on repository %v", repo) repoPath := repo.RepoPath() - log.Trace("Running health check on repository %s", repoPath) - if err := git.Fsck(repoPath, setting.Cron.RepoHealthCheck.Timeout, setting.Cron.RepoHealthCheck.Args...); err != nil { - desc := fmt.Sprintf("Failed to health check repository (%s): %v", repoPath, err) - log.Warn(desc) - if err = models.CreateRepositoryNotice(desc); err != nil { + if err := git.Fsck(ctx, repoPath, timeout, args...); err != nil { + log.Warn("Failed to health check repository (%v): %v", repo, err) + if err = models.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil { log.Error("CreateRepositoryNotice: %v", err) } } return nil }, ); err != nil { + log.Trace("Error: GitFsck: %v", err) return err } @@ -53,32 +53,43 @@ func GitFsck(ctx context.Context) error { } // GitGcRepos calls 'git gc' to remove unnecessary files and optimize the local repository -func GitGcRepos(ctx context.Context) error { +func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) error { log.Trace("Doing: GitGcRepos") - args := append([]string{"gc"}, setting.Git.GCArgs...) + args = append([]string{"gc"}, args...) if err := models.Iterate( models.DefaultDBContext(), new(models.Repository), builder.Gt{"id": 0}, func(idx int, bean interface{}) error { + repo := bean.(*models.Repository) select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown") + return models.ErrCancelledf("before GC of %s", repo.FullName()) default: } - - repo := bean.(*models.Repository) - if err := repo.GetOwner(); err != nil { - return err + log.Trace("Running git gc on %v", repo) + command := git.NewCommandContext(ctx, args...). + SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())) + var stdout string + var err error + if timeout > 0 { + var stdoutBytes []byte + stdoutBytes, err = command.RunInDirTimeout( + timeout, + repo.RepoPath()) + stdout = string(stdoutBytes) + } else { + stdout, err = command.RunInDir(repo.RepoPath()) } - if stdout, err := git.NewCommand(args...). - SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())). - RunInDirTimeout( - time.Duration(setting.Git.Timeout.GC)*time.Second, - repo.RepoPath()); err != nil { + + if err != nil { log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) - return fmt.Errorf("Repository garbage collection failed: Error: %v", err) + desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) + if err = models.CreateRepositoryNotice(desc); err != nil { + log.Error("CreateRepositoryNotice: %v", err) + } + return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %v", repo.FullName(), err) } return nil }, @@ -90,7 +101,7 @@ func GitGcRepos(ctx context.Context) error { return nil } -func gatherMissingRepoRecords() ([]*models.Repository, error) { +func gatherMissingRepoRecords(ctx context.Context) ([]*models.Repository, error) { repos := make([]*models.Repository, 0, 10) if err := models.Iterate( models.DefaultDBContext(), @@ -98,24 +109,33 @@ func gatherMissingRepoRecords() ([]*models.Repository, error) { builder.Gt{"id": 0}, func(idx int, bean interface{}) error { repo := bean.(*models.Repository) + select { + case <-ctx.Done(): + return models.ErrCancelledf("during gathering missing repo records before checking %s", repo.FullName()) + default: + } if !com.IsDir(repo.RepoPath()) { repos = append(repos, repo) } return nil }, ); err != nil { - if err2 := models.CreateRepositoryNotice(fmt.Sprintf("gatherMissingRepoRecords: %v", err)); err2 != nil { - return nil, fmt.Errorf("CreateRepositoryNotice: %v", err) + if strings.HasPrefix("Aborted gathering missing repo", err.Error()) { + return nil, err } + if err2 := models.CreateRepositoryNotice("gatherMissingRepoRecords: %v", err); err2 != nil { + log.Error("CreateRepositoryNotice: %v", err2) + } + return nil, err } return repos, nil } // DeleteMissingRepositories deletes all repository records that lost Git files. -func DeleteMissingRepositories(doer *models.User) error { - repos, err := gatherMissingRepoRecords() +func DeleteMissingRepositories(ctx context.Context, doer *models.User) error { + repos, err := gatherMissingRepoRecords(ctx) if err != nil { - return fmt.Errorf("gatherMissingRepoRecords: %v", err) + return err } if len(repos) == 0 { @@ -123,10 +143,16 @@ func DeleteMissingRepositories(doer *models.User) error { } for _, repo := range repos { + select { + case <-ctx.Done(): + return models.ErrCancelledf("during DeleteMissingRepositories before %s", repo.FullName()) + default: + } log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { - if err2 := models.CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil { - return fmt.Errorf("CreateRepositoryNotice: %v", err) + log.Error("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err) + if err2 := models.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { + log.Error("CreateRepositoryNotice: %v", err) } } } @@ -134,10 +160,10 @@ func DeleteMissingRepositories(doer *models.User) error { } // ReinitMissingRepositories reinitializes all repository records that lost Git files. -func ReinitMissingRepositories() error { - repos, err := gatherMissingRepoRecords() +func ReinitMissingRepositories(ctx context.Context) error { + repos, err := gatherMissingRepoRecords(ctx) if err != nil { - return fmt.Errorf("gatherMissingRepoRecords: %v", err) + return err } if len(repos) == 0 { @@ -145,10 +171,16 @@ func ReinitMissingRepositories() error { } for _, repo := range repos { + select { + case <-ctx.Done(): + return models.ErrCancelledf("during ReinitMissingRepositories before %s", repo.FullName()) + default: + } log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) if err := git.InitRepository(repo.RepoPath(), true); err != nil { - if err2 := models.CreateRepositoryNotice(fmt.Sprintf("InitRepository [%d]: %v", repo.ID, err)); err2 != nil { - return fmt.Errorf("CreateRepositoryNotice: %v", err) + log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RepoPath(), err) + if err2 := models.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { + log.Error("CreateRepositoryNotice: %v", err2) } } } diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go index 94f570bf3..6050f21f7 100644 --- a/modules/repository/hooks.go +++ b/modules/repository/hooks.go @@ -160,17 +160,18 @@ func SyncRepositoryHooks(ctx context.Context) error { new(models.Repository), builder.Gt{"id": 0}, func(idx int, bean interface{}) error { + repo := bean.(*models.Repository) select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown") + return models.ErrCancelledf("before sync repository hooks for %s", repo.FullName()) default: } - if err := createDelegateHooks(bean.(*models.Repository).RepoPath()); err != nil { + if err := createDelegateHooks(repo.RepoPath()); err != nil { return fmt.Errorf("SyncRepositoryHook: %v", err) } - if bean.(*models.Repository).HasWiki() { - if err := createDelegateHooks(bean.(*models.Repository).WikiPath()); err != nil { + if repo.HasWiki() { + if err := createDelegateHooks(repo.WikiPath()); err != nil { return fmt.Errorf("SyncRepositoryHook: %v", err) } } diff --git a/modules/setting/cron.go b/modules/setting/cron.go index 77f55168a..c8228ddaa 100644 --- a/modules/setting/cron.go +++ b/modules/setting/cron.go @@ -4,129 +4,8 @@ package setting -import ( - "time" - - "code.gitea.io/gitea/modules/log" -) - -var ( - - // Cron tasks - Cron = struct { - UpdateMirror struct { - Enabled bool - RunAtStart bool - Schedule string - } `ini:"cron.update_mirrors"` - RepoHealthCheck struct { - Enabled bool - RunAtStart bool - Schedule string - Timeout time.Duration - Args []string `delim:" "` - } `ini:"cron.repo_health_check"` - CheckRepoStats struct { - Enabled bool - RunAtStart bool - Schedule string - } `ini:"cron.check_repo_stats"` - ArchiveCleanup struct { - Enabled bool - RunAtStart bool - Schedule string - OlderThan time.Duration - } `ini:"cron.archive_cleanup"` - SyncExternalUsers struct { - Enabled bool - RunAtStart bool - Schedule string - UpdateExisting bool - } `ini:"cron.sync_external_users"` - DeletedBranchesCleanup struct { - Enabled bool - RunAtStart bool - Schedule string - OlderThan time.Duration - } `ini:"cron.deleted_branches_cleanup"` - UpdateMigrationPosterID struct { - Schedule string - } `ini:"cron.update_migration_poster_id"` - }{ - UpdateMirror: struct { - Enabled bool - RunAtStart bool - Schedule string - }{ - Enabled: true, - RunAtStart: false, - Schedule: "@every 10m", - }, - RepoHealthCheck: struct { - Enabled bool - RunAtStart bool - Schedule string - Timeout time.Duration - Args []string `delim:" "` - }{ - Enabled: true, - RunAtStart: false, - Schedule: "@every 24h", - Timeout: 60 * time.Second, - Args: []string{}, - }, - CheckRepoStats: struct { - Enabled bool - RunAtStart bool - Schedule string - }{ - Enabled: true, - RunAtStart: true, - Schedule: "@every 24h", - }, - ArchiveCleanup: struct { - Enabled bool - RunAtStart bool - Schedule string - OlderThan time.Duration - }{ - Enabled: true, - RunAtStart: true, - Schedule: "@every 24h", - OlderThan: 24 * time.Hour, - }, - SyncExternalUsers: struct { - Enabled bool - RunAtStart bool - Schedule string - UpdateExisting bool - }{ - Enabled: true, - RunAtStart: false, - Schedule: "@every 24h", - UpdateExisting: true, - }, - DeletedBranchesCleanup: struct { - Enabled bool - RunAtStart bool - Schedule string - OlderThan time.Duration - }{ - Enabled: true, - RunAtStart: true, - Schedule: "@every 24h", - OlderThan: 24 * time.Hour, - }, - UpdateMigrationPosterID: struct { - Schedule string - }{ - Schedule: "@every 24h", - }, - } -) - -func newCron() { - if err := Cfg.Section("cron").MapTo(&Cron); err != nil { - log.Fatal("Failed to map Cron settings: %v", err) - } +// GetCronSettings maps the cron subsection to the provided config +func GetCronSettings(name string, config interface{}) (interface{}, error) { + err := Cfg.Section("cron." + name).MapTo(config) + return config, err } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index dd7dbd3fd..ede4687c8 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -981,7 +981,6 @@ func NewContext() { u.Path = path.Join(u.Path, "api", "swagger") API.SwaggerURL = u.String() - newCron() newGit() sec = Cfg.Section("mirror") diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 232315122..975027dcc 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1842,26 +1842,37 @@ dashboard.operation_switch = Switch dashboard.operation_run = Run dashboard.clean_unbind_oauth = Clean unbound OAuth connections dashboard.clean_unbind_oauth_success = All unbound OAuth connections have been deleted. -dashboard.delete_inactivate_accounts = Delete all unactivated accounts -dashboard.delete_inactivate_accounts_success = All unactivated accounts have been deleted. +dashboard.task.started=Started Task: %s +dashboard.task.process=Task: %s +dashboard.task.cancelled=Task: %s cancelled: %[3]s +dashboard.task.error=Error in Task: %s: %[3]s +dashboard.task.finished=Task: %s started by %s has finished +dashboard.task.unknown=Unknown task: %s +dashboard.cron.started=Started Cron: %[1]s +dashboard.cron.process=Cron: %[1]s +dashboard.cron.cancelled=Cron: %s cancelled: %[3]s +dashboard.cron.error=Error in Cron: %s: %[3]s +dashboard.cron.finished=Cron: %[1]s has finished +dashboard.delete_inactive_accounts = Delete all unactivated accounts +dashboard.delete_inactive_accounts.started = Delete all unactivated accounts task started. dashboard.delete_repo_archives = Delete all repository archives -dashboard.delete_repo_archives_success = All repository archives have been deleted. +dashboard.delete_repo_archives.started = Delete all repository archives task started. dashboard.delete_missing_repos = Delete all repositories missing their Git files -dashboard.delete_missing_repos_success = All repositories missing their Git files have been deleted. +dashboard.delete_missing_repos.started = Delete all repositories missing their Git files task started. dashboard.delete_generated_repository_avatars = Delete generated repository avatars -dashboard.delete_generated_repository_avatars_success = Generated repository avatars were deleted. +dashboard.update_mirrors = Update Mirrors +dashboard.repo_health_check = Health check all repositories +dashboard.check_repo_stats = Check all repository statistics +dashboard.archive_cleanup = Delete old repository archives +dashboard.deleted_branches_cleanup = Clean-up deleted branches +dashboard.update_migration_poster_id = Update migration poster IDs dashboard.git_gc_repos = Garbage collect all repositories -dashboard.git_gc_repos_success = All repositories have finished garbage collection. -dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.) -dashboard.resync_all_sshkeys_success = The public SSH keys controlled by Gitea have been updated. +dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. +dashboard.resync_all_sshkeys.desc = (Not needed for the built-in SSH server.) dashboard.resync_all_hooks = Resynchronize pre-receive, update and post-receive hooks of all repositories. -dashboard.resync_all_hooks_success = All pre-receive, update and post-receive repository hooks have been resynchronized. dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for which records exist -dashboard.reinit_missing_repos_success = All missing Git repositories for which records existed have been reinitialized. dashboard.sync_external_users = Synchronize external user data -dashboard.sync_external_users_started = External user data synchronization has started. dashboard.git_fsck = Execute health checks on all repositories -dashboard.git_fsck_started = Repository health checks have started. dashboard.server_uptime = Server Uptime dashboard.current_goroutine = Current Goroutines dashboard.current_memory_usage = Current Memory Usage @@ -2266,6 +2277,7 @@ notices.delete_selected = Delete Selected notices.delete_all = Delete All Notices notices.type = Type notices.type_1 = Repository +notices.type_2 = Task notices.desc = Description notices.op = Op. notices.delete_success = The system notices have been deleted. diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 1b4a8631c..7595f4929 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -21,11 +21,9 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/cron" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/services/mailer" @@ -124,22 +122,6 @@ func updateSystemStatus() { sysStatus.NumGC = m.NumGC } -// Operation Operation types. -type Operation int - -const ( - cleanInactivateUser Operation = iota + 1 - cleanRepoArchives - cleanMissingRepos - gitGCRepos - syncSSHAuthorizedKey - syncRepositoryUpdateHook - reinitMissingRepository - syncExternalUsers - gitFsck - deleteGeneratedRepositoryAvatars -) - // Dashboard show admin panel dashboard func Dashboard(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.dashboard") @@ -162,48 +144,13 @@ func DashboardPost(ctx *context.Context, form auth.AdminDashboardForm) { ctx.Data["SysStatus"] = sysStatus // Run operation. - if form.Op > 0 { - var err error - var success string - shutdownCtx := graceful.GetManager().ShutdownContext() - - switch Operation(form.Op) { - case cleanInactivateUser: - success = ctx.Tr("admin.dashboard.delete_inactivate_accounts_success") - err = models.DeleteInactivateUsers() - case cleanRepoArchives: - success = ctx.Tr("admin.dashboard.delete_repo_archives_success") - err = models.DeleteRepositoryArchives() - case cleanMissingRepos: - success = ctx.Tr("admin.dashboard.delete_missing_repos_success") - err = repo_module.DeleteMissingRepositories(ctx.User) - case gitGCRepos: - success = ctx.Tr("admin.dashboard.git_gc_repos_success") - err = repo_module.GitGcRepos(shutdownCtx) - case syncSSHAuthorizedKey: - success = ctx.Tr("admin.dashboard.resync_all_sshkeys_success") - err = models.RewriteAllPublicKeys() - case syncRepositoryUpdateHook: - success = ctx.Tr("admin.dashboard.resync_all_hooks_success") - err = repo_module.SyncRepositoryHooks(shutdownCtx) - case reinitMissingRepository: - success = ctx.Tr("admin.dashboard.reinit_missing_repos_success") - err = repo_module.ReinitMissingRepositories() - case syncExternalUsers: - success = ctx.Tr("admin.dashboard.sync_external_users_started") - go graceful.GetManager().RunWithShutdownContext(models.SyncExternalUsers) - case gitFsck: - success = ctx.Tr("admin.dashboard.git_fsck_started") - err = repo_module.GitFsck(shutdownCtx) - case deleteGeneratedRepositoryAvatars: - success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success") - err = models.RemoveRandomAvatars() - } - - if err != nil { - ctx.Flash.Error(err.Error()) + if form.Op != "" { + task := cron.GetTask(form.Op) + if task != nil { + go task.RunWithUser(ctx.User, nil) + ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) } else { - ctx.Flash.Success(success) + ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) } } diff --git a/routers/init.go b/routers/init.go index 72f55c809..c891ec314 100644 --- a/routers/init.go +++ b/routers/init.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/ssh" "code.gitea.io/gitea/modules/task" @@ -33,7 +34,9 @@ import ( mirror_service "code.gitea.io/gitea/services/mirror" pull_service "code.gitea.io/gitea/services/pull" + "gitea.com/macaron/i18n" "gitea.com/macaron/macaron" + unknwoni18n "github.com/unknwon/i18n" ) func checkRunMode() { @@ -79,6 +82,33 @@ func initDBEngine(ctx context.Context) (err error) { return nil } +// InitLocales loads the locales +func InitLocales() { + localeNames, err := options.Dir("locale") + + if err != nil { + log.Fatal("Failed to list locale files: %v", err) + } + localFiles := make(map[string][]byte) + + for _, name := range localeNames { + localFiles[name], err = options.Locale(name) + + if err != nil { + log.Fatal("Failed to load %s locale file. %v", name, err) + } + } + i18n.I18n(i18n.Options{ + SubURL: setting.AppSubURL, + Files: localFiles, + Langs: setting.Langs, + Names: setting.Names, + DefaultLang: "en-US", + Redirect: false, + CookieDomain: setting.SessionConfig.Domain, + }) +} + // GlobalInit is for global configuration reload-able. func GlobalInit(ctx context.Context) { setting.NewContext() @@ -91,6 +121,11 @@ func GlobalInit(ctx context.Context) { log.Trace("Custom path: %s", setting.CustomPath) log.Trace("Log path: %s", setting.LogRootPath) + // Setup i18n + InitLocales() + + log.Info("%s", unknwoni18n.Tr("en-US", "admin.dashboard.delete_repo_archives")) + NewServices() if setting.InstallLock { diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 82d8fc59b..165e7cd35 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -296,7 +296,7 @@ func Password(m *models.Mirror) string { } // Update checks and updates mirror repositories. -func Update(ctx context.Context) { +func Update(ctx context.Context) error { log.Trace("Doing: Update") if err := models.MirrorsIterate(func(idx int, bean interface{}) error { m := bean.(*models.Mirror) @@ -306,14 +306,17 @@ func Update(ctx context.Context) { } select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown") + return fmt.Errorf("Aborted") default: mirrorQueue.Add(m.RepoID) return nil } }); err != nil { - log.Error("Update: %v", err) + log.Trace("Update: %v", err) + return err } + log.Trace("Finished: Update") + return nil } // SyncMirrors checks and syncs mirrors. diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 151b48971..affed1634 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -15,49 +15,50 @@ {{.i18n.Tr "admin.dashboard.operations"}}
-
+ {{.CsrfTokenHtml}} - - + + - + - + - + - - + + - + - + - + - + - +
{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}{{.i18n.Tr "admin.dashboard.delete_inactive_accounts"}}
{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}
{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}
{{.i18n.Tr "admin.dashboard.git_gc_repos"}}
{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}
+ {{.i18n.Tr "admin.dashboard.resync_all_sshkeys.desc"}}
{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}
{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}
{{.i18n.Tr "admin.dashboard.sync_external_users"}}
{{.i18n.Tr "admin.dashboard.git_fsck"}}
{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}
diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl index 0f9c2150b..26fbcb60a 100644 --- a/templates/admin/monitor.tmpl +++ b/templates/admin/monitor.tmpl @@ -7,28 +7,33 @@ {{.i18n.Tr "admin.monitor.cron"}}
- - - - - - - - - - - - {{range .Entries}} + + {{.CsrfTokenHtml}} +
{{.i18n.Tr "admin.monitor.name"}}{{.i18n.Tr "admin.monitor.schedule"}}{{.i18n.Tr "admin.monitor.next"}}{{.i18n.Tr "admin.monitor.previous"}}{{.i18n.Tr "admin.monitor.execute_times"}}
+ - - - - - + + + + + + - {{end}} - -
{{.Description}}{{.Spec}}{{DateFmtLong .Next}}{{if gt .Prev.Year 1 }}{{DateFmtLong .Prev}}{{else}}N/A{{end}}{{.ExecTimes}}{{.i18n.Tr "admin.monitor.name"}}{{.i18n.Tr "admin.monitor.schedule"}}{{.i18n.Tr "admin.monitor.next"}}{{.i18n.Tr "admin.monitor.previous"}}{{.i18n.Tr "admin.monitor.execute_times"}}
+ + + {{range .Entries}} + + + {{$.i18n.Tr (printf "admin.dashboard.%s" .Name)}} + {{.Spec}} + {{DateFmtLong .Next}} + {{if gt .Prev.Year 1 }}{{DateFmtLong .Prev}}{{else}}N/A{{end}} + {{.ExecTimes}} + + {{end}} + + +