* 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 <art27@cantab.net> * fix name check Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @guillep2k * as per @lafriks Signed-off-by: Andrew Thornton <art27@cantab.net> * Add git.CommandContext variants Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>tags/v1.13.0-dev
} | } | ||||
defer prepareTestEnv(t)() | defer prepareTestEnv(t)() | ||||
addAuthSourceLDAP(t, "") | addAuthSourceLDAP(t, "") | ||||
models.SyncExternalUsers(context.Background()) | |||||
models.SyncExternalUsers(context.Background(), true) | |||||
session := loginUser(t, "user1") | session := loginUser(t, "user1") | ||||
// Check if users exists | // Check if users exists | ||||
defer prepareTestEnv(t)() | defer prepareTestEnv(t)() | ||||
addAuthSourceLDAP(t, "sshPublicKey") | addAuthSourceLDAP(t, "sshPublicKey") | ||||
models.SyncExternalUsers(context.Background()) | |||||
models.SyncExternalUsers(context.Background(), true) | |||||
// Check if users has SSH keys synced | // Check if users has SSH keys synced | ||||
for _, u := range gitLDAPUsers { | for _, u := range gitLDAPUsers { |
const ( | const ( | ||||
//NoticeRepository type | //NoticeRepository type | ||||
NoticeRepository NoticeType = iota + 1 | NoticeRepository NoticeType = iota + 1 | ||||
// NoticeTask type | |||||
NoticeTask | |||||
) | ) | ||||
// Notice represents a system notice for admin. | // Notice represents a system notice for admin. | ||||
} | } | ||||
// CreateNotice creates new system notice. | // 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{ | n := &Notice{ | ||||
Type: tp, | Type: tp, | ||||
Description: desc, | Description: desc, | ||||
} | } | ||||
// CreateRepositoryNotice creates new system notice with type NoticeRepository. | // 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 | // RemoveAllWithNotice removes all directories in given path and |
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/setting" | |||||
"code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
"code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
} | } | ||||
// RemoveOldDeletedBranches removes old deleted branches | // 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 | // Nothing to do for shutdown or terminate | ||||
log.Trace("Doing: DeletedBranchesCleanup") | 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)) | _, err := x.Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch)) | ||||
if err != nil { | if err != nil { | ||||
log.Error("DeletedBranchesCleanup: %v", err) | log.Error("DeletedBranchesCleanup: %v", err) |
return "SSH is disabled" | 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...), | |||||
} | |||||
} | |||||
// ____ ___ | // ____ ___ | ||||
// | | \______ ___________ | // | | \______ ___________ | ||||
// | | / ___// __ \_ __ \ | // | | / ___// __ \_ __ \ |
} | } | ||||
// DeleteRepositoryArchives deletes all repositories' archives. | // DeleteRepositoryArchives deletes all repositories' archives. | ||||
func DeleteRepositoryArchives() error { | |||||
func DeleteRepositoryArchives(ctx context.Context) error { | |||||
return x. | return x. | ||||
Where("id > 0"). | Where("id > 0"). | ||||
Iterate(new(Repository), | Iterate(new(Repository), | ||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
repo := bean.(*Repository) | 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")) | return os.RemoveAll(filepath.Join(repo.RepoPath(), "archives")) | ||||
}) | }) | ||||
} | } | ||||
// DeleteOldRepositoryArchives deletes old repository archives. | // DeleteOldRepositoryArchives deletes old repository archives. | ||||
func DeleteOldRepositoryArchives(ctx context.Context) { | |||||
func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) error { | |||||
log.Trace("Doing: ArchiveCleanup") | log.Trace("Doing: ArchiveCleanup") | ||||
if err := x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { | 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 { | }); 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) | repo := bean.(*Repository) | ||||
basePath := filepath.Join(repo.RepoPath(), "archives") | basePath := filepath.Join(repo.RepoPath(), "archives") | ||||
for _, ty := range []string{"zip", "targz"} { | for _, ty := range []string{"zip", "targz"} { | ||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
return err | return err | ||||
} | } | ||||
minimumOldestTime := time.Now().Add(-setting.Cron.ArchiveCleanup.OlderThan) | |||||
minimumOldestTime := time.Now().Add(-olderThan) | |||||
for _, info := range files { | for _, info := range files { | ||||
if info.ModTime().Before(minimumOldestTime) && !info.IsDir() { | if info.ModTime().Before(minimumOldestTime) && !info.IsDir() { | ||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
toDelete := filepath.Join(path, info.Name()) | toDelete := filepath.Join(path, info.Name()) | ||||
return | return | ||||
} | } | ||||
for _, result := range results { | for _, result := range results { | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
log.Warn("CheckRepoStats: Aborting due to shutdown") | |||||
log.Warn("CheckRepoStats: Cancelled before checking %s for Repo[%d]", checker.desc, id) | |||||
return | return | ||||
default: | default: | ||||
} | } | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
log.Trace("Updating %s: %d", checker.desc, id) | log.Trace("Updating %s: %d", checker.desc, id) | ||||
_, err = x.Exec(checker.correctSQL, id, id) | _, err = x.Exec(checker.correctSQL, id, id) | ||||
if err != nil { | if err != nil { | ||||
} | } | ||||
// CheckRepoStats checks the repository stats | // CheckRepoStats checks the repository stats | ||||
func CheckRepoStats(ctx context.Context) { | |||||
func CheckRepoStats(ctx context.Context) error { | |||||
log.Trace("Doing: CheckRepoStats") | log.Trace("Doing: CheckRepoStats") | ||||
checkers := []*repoChecker{ | checkers := []*repoChecker{ | ||||
"issue count 'num_comments'", | "issue count 'num_comments'", | ||||
}, | }, | ||||
} | } | ||||
for i := range checkers { | |||||
for _, checker := range checkers { | |||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
repoStatsCheck(ctx, checkers[i]) | |||||
repoStatsCheck(ctx, checker) | |||||
} | } | ||||
} | } | ||||
log.Error("Select %s: %v", desc, err) | log.Error("Select %s: %v", desc, err) | ||||
} else { | } else { | ||||
for _, result := range results { | for _, result := range results { | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
log.Trace("Updating %s: %d", desc, id) | 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) | _, 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 { | if err != nil { | ||||
log.Error("Select %s: %v", desc, err) | log.Error("Select %s: %v", desc, err) | ||||
} else { | } else { | ||||
for _, result := range results { | for _, result := range results { | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
log.Trace("Updating %s: %d", desc, id) | 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) | _, 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 { | if err != nil { | ||||
log.Error("Select repository count 'num_forks': %v", err) | log.Error("Select repository count 'num_forks': %v", err) | ||||
} else { | } else { | ||||
for _, result := range results { | for _, result := range results { | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
id := com.StrTo(result["id"]).MustInt64() | |||||
log.Trace("Updating repository count 'num_forks': %d", id) | log.Trace("Updating repository count 'num_forks': %d", id) | ||||
repo, err := GetRepositoryByID(id) | repo, err := GetRepositoryByID(id) | ||||
} | } | ||||
} | } | ||||
// ***** END: Repository.NumForks ***** | // ***** END: Repository.NumForks ***** | ||||
return nil | |||||
} | } | ||||
// SetArchiveRepoState sets if a repo is archived | // SetArchiveRepoState sets if a repo is archived | ||||
} | } | ||||
// RemoveRandomAvatars removes the randomly generated avatars that were created for repositories | // RemoveRandomAvatars removes the randomly generated avatars that were created for repositories | ||||
func RemoveRandomAvatars() error { | |||||
func RemoveRandomAvatars(ctx context.Context) error { | |||||
return x. | return x. | ||||
Where("id > 0").BufferSize(setting.Database.IterateBufferSize). | Where("id > 0").BufferSize(setting.Database.IterateBufferSize). | ||||
Iterate(new(Repository), | Iterate(new(Repository), | ||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
repository := bean.(*Repository) | repository := bean.(*Repository) | ||||
select { | |||||
case <-ctx.Done(): | |||||
return ErrCancelledf("before random avatars removed for %s", repository.FullName()) | |||||
default: | |||||
} | |||||
stringifiedID := strconv.FormatInt(repository.ID, 10) | stringifiedID := strconv.FormatInt(repository.ID, 10) | ||||
if repository.Avatar == stringifiedID { | if repository.Avatar == stringifiedID { | ||||
return repository.DeleteAvatar() | return repository.DeleteAvatar() |
return sess.Commit() | 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) | 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. | // FIXME: should only update authorized_keys file once after all deletions. | ||||
for _, u := range users { | for _, u := range users { | ||||
select { | |||||
case <-ctx.Done(): | |||||
return ErrCancelledf("Before delete inactive user %s", u.Name) | |||||
default: | |||||
} | |||||
if err = DeleteUser(u); err != nil { | if err = DeleteUser(u); err != nil { | ||||
// Ignore users that were set inactive by admin. | // Ignore users that were set inactive by admin. | ||||
if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) { | if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) { | ||||
} | } | ||||
// SyncExternalUsers is used to synchronize users with external authorization source | // 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") | log.Trace("Doing: SyncExternalUsers") | ||||
ls, err := LoginSources() | ls, err := LoginSources() | ||||
if err != nil { | if err != nil { | ||||
log.Error("SyncExternalUsers: %v", err) | log.Error("SyncExternalUsers: %v", err) | ||||
return | |||||
return err | |||||
} | } | ||||
updateExisting := setting.Cron.SyncExternalUsers.UpdateExisting | |||||
for _, s := range ls { | for _, s := range ls { | ||||
if !s.IsActived || !s.IsSyncEnabled { | if !s.IsActived || !s.IsSyncEnabled { | ||||
continue | continue | ||||
} | } | ||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
Find(&users) | Find(&users) | ||||
if err != nil { | if err != nil { | ||||
log.Error("SyncExternalUsers: %v", err) | log.Error("SyncExternalUsers: %v", err) | ||||
return | |||||
return err | |||||
} | } | ||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
for _, su := range sr { | for _, su := range sr { | ||||
select { | select { | ||||
case <-ctx.Done(): | 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 | // Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed | ||||
if sshKeysNeedUpdate { | if sshKeysNeedUpdate { | ||||
err = RewriteAllPublicKeys() | err = RewriteAllPublicKeys() | ||||
log.Error("RewriteAllPublicKeys: %v", err) | log.Error("RewriteAllPublicKeys: %v", err) | ||||
} | } | ||||
} | } | ||||
return | |||||
return ErrCancelledf("During update of %s before completed update of users", s.Name) | |||||
default: | default: | ||||
} | } | ||||
if len(su.Username) == 0 { | if len(su.Username) == 0 { | ||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
return nil | |||||
} | } |
// AdminDashboardForm form for admin dashboard operations | // AdminDashboardForm form for admin dashboard operations | ||||
type AdminDashboardForm struct { | type AdminDashboardForm struct { | ||||
Op int `binding:"required"` | |||||
Op string `binding:"required"` | |||||
} | } | ||||
// Validate validates form fields | // Validate validates form fields |
"context" | "context" | ||||
"time" | "time" | ||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/graceful" | "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" | "code.gitea.io/gitea/modules/sync" | ||||
mirror_service "code.gitea.io/gitea/services/mirror" | |||||
"github.com/gogs/cron" | "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() | var c = cron.New() | ||||
// Prevent duplicate running tasks. | // Prevent duplicate running tasks. | ||||
var taskStatusTable = sync.NewStatusTable() | 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 | // NewContext begins cron tasks | ||||
// Each cron task is run within the shutdown context as a running server | // Each cron task is run within the shutdown context as a running server | ||||
// AtShutdown the cron server is stopped | // AtShutdown the cron server is stopped | ||||
func NewContext() { | 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() | 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() | |||||
}) | |||||
} | |||||
// 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. | // ListTasks returns all running cron tasks. | ||||
func ListTasks() []*cron.Entry { | |||||
return c.Entries() | |||||
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 | |||||
} | } |
// 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...) | |||||
} |
// 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) | |||||
} | |||||
} |
// 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() | |||||
} |
// 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() | |||||
} |
// NewCommand creates and returns a new Git Command based on given command and arguments. | // NewCommand creates and returns a new Git Command based on given command and arguments. | ||||
func NewCommand(args ...string) *Command { | 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 | // Make an explicit copy of GlobalCommandArgs, otherwise append might overwrite it | ||||
cargs := make([]string, len(GlobalCommandArgs)) | cargs := make([]string, len(GlobalCommandArgs)) | ||||
copy(cargs, GlobalCommandArgs) | copy(cargs, GlobalCommandArgs) | ||||
return &Command{ | return &Command{ | ||||
name: GitExecutable, | name: GitExecutable, | ||||
args: append(cargs, args...), | 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 | // 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 { | 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{ | return &Command{ | ||||
name: GitExecutable, | name: GitExecutable, | ||||
args: args, | args: args, | ||||
parentContext: DefaultContext, | |||||
parentContext: ctx, | |||||
} | } | ||||
} | } | ||||
} | } | ||||
// Fsck verifies the connectivity and validity of the objects in the database | // 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. | // Make sure timeout makes sense. | ||||
if timeout <= 0 { | if timeout <= 0 { | ||||
timeout = -1 | timeout = -1 | ||||
} | } | ||||
_, err := NewCommand("fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath) | |||||
_, err := NewCommandContext(ctx, "fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath) | |||||
return err | return err | ||||
} | } |
) | ) | ||||
// UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID | // 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 { | for _, gitService := range structs.SupportedFullGitService { | ||||
select { | select { | ||||
case <-ctx.Done(): | 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: | default: | ||||
} | } | ||||
if err := updateMigrationPosterIDByGitService(ctx, gitService); err != nil { | if err := updateMigrationPosterIDByGitService(ctx, gitService); err != nil { | ||||
log.Error("updateMigrationPosterIDByGitService failed: %v", err) | log.Error("updateMigrationPosterIDByGitService failed: %v", err) | ||||
} | } | ||||
} | } | ||||
return nil | |||||
} | } | ||||
func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServiceType) error { | func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServiceType) error { | ||||
for { | for { | ||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
log.Warn("UpdateMigrationPosterIDByGitService(%s) aborted due to shutdown", tp.Name()) | |||||
log.Warn("UpdateMigrationPosterIDByGitService(%s) cancelled", tp.Name()) | |||||
return nil | return nil | ||||
default: | default: | ||||
} | } | ||||
for _, user := range users { | for _, user := range users { | ||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
log.Warn("UpdateMigrationPosterIDByGitService(%s) aborted due to shutdown", tp.Name()) | |||||
log.Warn("UpdateMigrationPosterIDByGitService(%s) cancelled", tp.Name()) | |||||
return nil | return nil | ||||
default: | default: | ||||
} | } |
import ( | import ( | ||||
"context" | "context" | ||||
"fmt" | "fmt" | ||||
"strings" | |||||
"time" | "time" | ||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/setting" | |||||
"github.com/unknwon/com" | "github.com/unknwon/com" | ||||
"xorm.io/builder" | "xorm.io/builder" | ||||
) | ) | ||||
// GitFsck calls 'git fsck' to check repository health. | // 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") | log.Trace("Doing: GitFsck") | ||||
if err := models.Iterate( | if err := models.Iterate( | ||||
new(models.Repository), | new(models.Repository), | ||||
builder.Expr("id>0 AND is_fsck_enabled=?", true), | builder.Expr("id>0 AND is_fsck_enabled=?", true), | ||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
repo := bean.(*models.Repository) | |||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
return fmt.Errorf("Aborted due to shutdown") | |||||
return models.ErrCancelledf("before fsck of %s", repo.FullName()) | |||||
default: | default: | ||||
} | } | ||||
repo := bean.(*models.Repository) | |||||
log.Trace("Running health check on repository %v", repo) | |||||
repoPath := repo.RepoPath() | 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) | log.Error("CreateRepositoryNotice: %v", err) | ||||
} | } | ||||
} | } | ||||
return nil | return nil | ||||
}, | }, | ||||
); err != nil { | ); err != nil { | ||||
log.Trace("Error: GitFsck: %v", err) | |||||
return err | return err | ||||
} | } | ||||
} | } | ||||
// GitGcRepos calls 'git gc' to remove unnecessary files and optimize the local repository | // 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") | log.Trace("Doing: GitGcRepos") | ||||
args := append([]string{"gc"}, setting.Git.GCArgs...) | |||||
args = append([]string{"gc"}, args...) | |||||
if err := models.Iterate( | if err := models.Iterate( | ||||
models.DefaultDBContext(), | models.DefaultDBContext(), | ||||
new(models.Repository), | new(models.Repository), | ||||
builder.Gt{"id": 0}, | builder.Gt{"id": 0}, | ||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
repo := bean.(*models.Repository) | |||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
return fmt.Errorf("Aborted due to shutdown") | |||||
return models.ErrCancelledf("before GC of %s", repo.FullName()) | |||||
default: | 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) | 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 | return nil | ||||
}, | }, | ||||
return nil | return nil | ||||
} | } | ||||
func gatherMissingRepoRecords() ([]*models.Repository, error) { | |||||
func gatherMissingRepoRecords(ctx context.Context) ([]*models.Repository, error) { | |||||
repos := make([]*models.Repository, 0, 10) | repos := make([]*models.Repository, 0, 10) | ||||
if err := models.Iterate( | if err := models.Iterate( | ||||
models.DefaultDBContext(), | models.DefaultDBContext(), | ||||
builder.Gt{"id": 0}, | builder.Gt{"id": 0}, | ||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
repo := bean.(*models.Repository) | 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()) { | if !com.IsDir(repo.RepoPath()) { | ||||
repos = append(repos, repo) | repos = append(repos, repo) | ||||
} | } | ||||
return nil | return nil | ||||
}, | }, | ||||
); err != 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 | return repos, nil | ||||
} | } | ||||
// DeleteMissingRepositories deletes all repository records that lost Git files. | // 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 { | if err != nil { | ||||
return fmt.Errorf("gatherMissingRepoRecords: %v", err) | |||||
return err | |||||
} | } | ||||
if len(repos) == 0 { | if len(repos) == 0 { | ||||
} | } | ||||
for _, repo := range repos { | 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) | log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) | ||||
if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { | 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) | |||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// ReinitMissingRepositories reinitializes all repository records that lost Git files. | // 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 { | if err != nil { | ||||
return fmt.Errorf("gatherMissingRepoRecords: %v", err) | |||||
return err | |||||
} | } | ||||
if len(repos) == 0 { | if len(repos) == 0 { | ||||
} | } | ||||
for _, repo := range repos { | 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) | log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) | ||||
if err := git.InitRepository(repo.RepoPath(), true); err != nil { | 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) | |||||
} | } | ||||
} | } | ||||
} | } |
new(models.Repository), | new(models.Repository), | ||||
builder.Gt{"id": 0}, | builder.Gt{"id": 0}, | ||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
repo := bean.(*models.Repository) | |||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
return fmt.Errorf("Aborted due to shutdown") | |||||
return models.ErrCancelledf("before sync repository hooks for %s", repo.FullName()) | |||||
default: | default: | ||||
} | } | ||||
if err := createDelegateHooks(bean.(*models.Repository).RepoPath()); err != nil { | |||||
if err := createDelegateHooks(repo.RepoPath()); err != nil { | |||||
return fmt.Errorf("SyncRepositoryHook: %v", err) | 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) | return fmt.Errorf("SyncRepositoryHook: %v", err) | ||||
} | } | ||||
} | } |
package setting | 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 | |||||
} | } |
u.Path = path.Join(u.Path, "api", "swagger") | u.Path = path.Join(u.Path, "api", "swagger") | ||||
API.SwaggerURL = u.String() | API.SwaggerURL = u.String() | ||||
newCron() | |||||
newGit() | newGit() | ||||
sec = Cfg.Section("mirror") | sec = Cfg.Section("mirror") |
dashboard.operation_run = Run | dashboard.operation_run = Run | ||||
dashboard.clean_unbind_oauth = Clean unbound OAuth connections | dashboard.clean_unbind_oauth = Clean unbound OAuth connections | ||||
dashboard.clean_unbind_oauth_success = All unbound OAuth connections have been deleted. | 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = Execute health checks on all repositories | ||||
dashboard.git_fsck_started = Repository health checks have started. | |||||
dashboard.server_uptime = Server Uptime | dashboard.server_uptime = Server Uptime | ||||
dashboard.current_goroutine = Current Goroutines | dashboard.current_goroutine = Current Goroutines | ||||
dashboard.current_memory_usage = Current Memory Usage | dashboard.current_memory_usage = Current Memory Usage | ||||
notices.delete_all = Delete All Notices | notices.delete_all = Delete All Notices | ||||
notices.type = Type | notices.type = Type | ||||
notices.type_1 = Repository | notices.type_1 = Repository | ||||
notices.type_2 = Task | |||||
notices.desc = Description | notices.desc = Description | ||||
notices.op = Op. | notices.op = Op. | ||||
notices.delete_success = The system notices have been deleted. | notices.delete_success = The system notices have been deleted. |
"code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
"code.gitea.io/gitea/modules/cron" | "code.gitea.io/gitea/modules/cron" | ||||
"code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
"code.gitea.io/gitea/modules/graceful" | |||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/process" | "code.gitea.io/gitea/modules/process" | ||||
"code.gitea.io/gitea/modules/queue" | "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/setting" | ||||
"code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
"code.gitea.io/gitea/services/mailer" | "code.gitea.io/gitea/services/mailer" | ||||
sysStatus.NumGC = m.NumGC | 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 | // Dashboard show admin panel dashboard | ||||
func Dashboard(ctx *context.Context) { | func Dashboard(ctx *context.Context) { | ||||
ctx.Data["Title"] = ctx.Tr("admin.dashboard") | ctx.Data["Title"] = ctx.Tr("admin.dashboard") | ||||
ctx.Data["SysStatus"] = sysStatus | ctx.Data["SysStatus"] = sysStatus | ||||
// Run operation. | // 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 { | } else { | ||||
ctx.Flash.Success(success) | |||||
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) | |||||
} | } | ||||
} | } | ||||
"code.gitea.io/gitea/modules/markup" | "code.gitea.io/gitea/modules/markup" | ||||
"code.gitea.io/gitea/modules/markup/external" | "code.gitea.io/gitea/modules/markup/external" | ||||
"code.gitea.io/gitea/modules/notification" | "code.gitea.io/gitea/modules/notification" | ||||
"code.gitea.io/gitea/modules/options" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"code.gitea.io/gitea/modules/ssh" | "code.gitea.io/gitea/modules/ssh" | ||||
"code.gitea.io/gitea/modules/task" | "code.gitea.io/gitea/modules/task" | ||||
mirror_service "code.gitea.io/gitea/services/mirror" | mirror_service "code.gitea.io/gitea/services/mirror" | ||||
pull_service "code.gitea.io/gitea/services/pull" | pull_service "code.gitea.io/gitea/services/pull" | ||||
"gitea.com/macaron/i18n" | |||||
"gitea.com/macaron/macaron" | "gitea.com/macaron/macaron" | ||||
unknwoni18n "github.com/unknwon/i18n" | |||||
) | ) | ||||
func checkRunMode() { | func checkRunMode() { | ||||
return nil | 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. | // GlobalInit is for global configuration reload-able. | ||||
func GlobalInit(ctx context.Context) { | func GlobalInit(ctx context.Context) { | ||||
setting.NewContext() | setting.NewContext() | ||||
log.Trace("Custom path: %s", setting.CustomPath) | log.Trace("Custom path: %s", setting.CustomPath) | ||||
log.Trace("Log path: %s", setting.LogRootPath) | log.Trace("Log path: %s", setting.LogRootPath) | ||||
// Setup i18n | |||||
InitLocales() | |||||
log.Info("%s", unknwoni18n.Tr("en-US", "admin.dashboard.delete_repo_archives")) | |||||
NewServices() | NewServices() | ||||
if setting.InstallLock { | if setting.InstallLock { |
} | } | ||||
// Update checks and updates mirror repositories. | // Update checks and updates mirror repositories. | ||||
func Update(ctx context.Context) { | |||||
func Update(ctx context.Context) error { | |||||
log.Trace("Doing: Update") | log.Trace("Doing: Update") | ||||
if err := models.MirrorsIterate(func(idx int, bean interface{}) error { | if err := models.MirrorsIterate(func(idx int, bean interface{}) error { | ||||
m := bean.(*models.Mirror) | m := bean.(*models.Mirror) | ||||
} | } | ||||
select { | select { | ||||
case <-ctx.Done(): | case <-ctx.Done(): | ||||
return fmt.Errorf("Aborted due to shutdown") | |||||
return fmt.Errorf("Aborted") | |||||
default: | default: | ||||
mirrorQueue.Add(m.RepoID) | mirrorQueue.Add(m.RepoID) | ||||
return nil | return nil | ||||
} | } | ||||
}); err != 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. | // SyncMirrors checks and syncs mirrors. |
{{.i18n.Tr "admin.dashboard.operations"}} | {{.i18n.Tr "admin.dashboard.operations"}} | ||||
</h4> | </h4> | ||||
<div class="ui attached table segment"> | <div class="ui attached table segment"> | ||||
<form method="post" action="{{AppSubUrl}}/admin"> | |||||
<form method="post" action="{{.AppSubUrl}}/admin"> | |||||
{{.CsrfTokenHtml}} | {{.CsrfTokenHtml}} | ||||
<table class="ui very basic table"> | <table class="ui very basic table"> | ||||
<tbody> | <tbody> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}</td> | |||||
<td><button type="submit" class="ui green button" name="op" value="1">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td>{{.i18n.Tr "admin.dashboard.delete_inactive_accounts"}}</td> | |||||
<td><button type="submit" class="ui green button" name="op" value="delete_inactive_accounts">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td> | <td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="2">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="delete_repo_archives">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td> | <td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="3">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="delete_missing_repos">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td> | <td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="4">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="git_gc_repos">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}</td> | |||||
<td><button type="submit" class="ui green button" name="op" value="5">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}<br/> | |||||
{{.i18n.Tr "admin.dashboard.resync_all_sshkeys.desc"}}</td> | |||||
<td><button type="submit" class="ui green button" name="op" value="resync_all_sshkeys">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td> | <td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="6">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="resync_all_hooks">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td> | <td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="7">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="reinit_missing_repos">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td> | <td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="8">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="sync_external_users">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td> | <td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="9">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="git_fsck">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td> | <td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td> | ||||
<td><button type="submit" class="ui green button" name="op" value="10">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
<td><button type="submit" class="ui green button" name="op" value="delete_generated_repository_avatars">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | |||||
</tr> | </tr> | ||||
</tbody> | </tbody> | ||||
</table> | </table> |
{{.i18n.Tr "admin.monitor.cron"}} | {{.i18n.Tr "admin.monitor.cron"}} | ||||
</h4> | </h4> | ||||
<div class="ui attached table segment"> | <div class="ui attached table segment"> | ||||
<table class="ui very basic striped table"> | |||||
<thead> | |||||
<tr> | |||||
<th>{{.i18n.Tr "admin.monitor.name"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.schedule"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.next"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.previous"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.execute_times"}}</th> | |||||
</tr> | |||||
</thead> | |||||
<tbody> | |||||
{{range .Entries}} | |||||
<form method="post" action="{{.AppSubUrl}}/admin"> | |||||
{{.CsrfTokenHtml}} | |||||
<table class="ui very basic striped table"> | |||||
<thead> | |||||
<tr> | <tr> | ||||
<td>{{.Description}}</td> | |||||
<td>{{.Spec}}</td> | |||||
<td>{{DateFmtLong .Next}}</td> | |||||
<td>{{if gt .Prev.Year 1 }}{{DateFmtLong .Prev}}{{else}}N/A{{end}}</td> | |||||
<td>{{.ExecTimes}}</td> | |||||
<th></th> | |||||
<th>{{.i18n.Tr "admin.monitor.name"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.schedule"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.next"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.previous"}}</th> | |||||
<th>{{.i18n.Tr "admin.monitor.execute_times"}}</th> | |||||
</tr> | </tr> | ||||
{{end}} | |||||
</tbody> | |||||
</table> | |||||
</thead> | |||||
<tbody> | |||||
{{range .Entries}} | |||||
<tr> | |||||
<td><button type="submit" class="ui green button" name="op" value="{{.Name}}" title="{{$.i18n.Tr "admin.dashboard.operation_run"}}">{{svg "octicon-triangle-right" 16}}</button></td> | |||||
<td>{{$.i18n.Tr (printf "admin.dashboard.%s" .Name)}}</td> | |||||
<td>{{.Spec}}</td> | |||||
<td>{{DateFmtLong .Next}}</td> | |||||
<td>{{if gt .Prev.Year 1 }}{{DateFmtLong .Prev}}{{else}}N/A{{end}}</td> | |||||
<td>{{.ExecTimes}}</td> | |||||
</tr> | |||||
{{end}} | |||||
</tbody> | |||||
</table> | |||||
</form> | |||||
</div> | </div> | ||||
<h4 class="ui top attached header"> | <h4 class="ui top attached header"> |