2020-05-17 02:31:38 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-05-17 02:31:38 +03:00
package cron
import (
"context"
"fmt"
"reflect"
2023-07-24 07:13:41 +03:00
"strings"
2020-05-17 02:31:38 +03:00
"sync"
2023-10-11 10:28:16 +03:00
"time"
2020-05-17 02:31:38 +03:00
2021-11-10 08:13:16 +03:00
"code.gitea.io/gitea/models/db"
2022-10-17 02:29:26 +03:00
system_model "code.gitea.io/gitea/models/system"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2020-05-17 02:31:38 +03:00
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
2022-02-07 10:43:53 +03:00
"code.gitea.io/gitea/modules/translation"
2020-05-17 02:31:38 +03:00
)
2022-01-20 20:46:10 +03:00
var (
lock = sync . Mutex { }
started = false
tasks = [ ] * Task { }
tasksMap = map [ string ] * Task { }
)
2020-05-17 02:31:38 +03:00
// Task represents a Cron task
type Task struct {
2022-03-29 04:31:07 +03:00
lock sync . Mutex
Name string
config Config
fun func ( context . Context , * user_model . User , Config ) error
Status string
LastMessage string
LastDoer string
ExecTimes int64
2023-10-11 10:28:16 +03:00
// This stores the time of the last manual run of this task.
LastRun time . Time
2020-05-17 02:31:38 +03:00
}
// 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 ( ) {
2021-11-24 12:49:20 +03:00
t . RunWithUser ( & user_model . User {
2020-05-17 02:31:38 +03:00
ID : - 1 ,
Name : "(Cron)" ,
LowerName : "(cron)" ,
} , t . config )
}
// RunWithUser will run the task incrementing the cron counter at the time with User
2021-11-24 12:49:20 +03:00
func ( t * Task ) RunWithUser ( doer * user_model . User , config Config ) {
2020-05-17 02:31:38 +03:00
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 )
} ( )
graceful . GetManager ( ) . RunWithShutdownContext ( func ( baseCtx context . Context ) {
2023-12-10 23:15:06 +03:00
defer func ( ) {
if err := recover ( ) ; err != nil {
// Recover a panic within the execution of the task.
combinedErr := fmt . Errorf ( "%s\n%s" , err , log . Stack ( 2 ) )
log . Error ( "PANIC whilst running task: %s Value: %v" , t . Name , combinedErr )
}
} ( )
2023-10-11 10:28:16 +03:00
// Store the time of this run, before the function is executed, so it
// matches the behavior of what the cron library does.
t . lock . Lock ( )
t . LastRun = time . Now ( )
t . lock . Unlock ( )
2020-05-17 02:31:38 +03:00
pm := process . GetManager ( )
2022-03-29 04:31:07 +03:00
doerName := ""
if doer != nil && doer . ID != - 1 {
doerName = doer . Name
}
2022-06-26 17:19:22 +03:00
ctx , _ , finished := pm . AddContext ( baseCtx , config . FormatMessage ( translation . NewLocale ( "en-US" ) , t . Name , "process" , doerName ) )
2021-11-30 23:06:32 +03:00
defer finished ( )
2020-05-17 02:31:38 +03:00
if err := t . fun ( ctx , doer , config ) ; err != nil {
2022-03-29 04:31:07 +03:00
var message string
var status string
2021-11-10 08:13:16 +03:00
if db . IsErrCancelled ( err ) {
2022-03-29 04:31:07 +03:00
status = "cancelled"
message = err . ( db . ErrCancelled ) . Message
} else {
status = "error"
message = err . Error ( )
2020-05-17 02:31:38 +03:00
}
2022-03-29 04:31:07 +03:00
t . lock . Lock ( )
t . LastMessage = message
t . Status = status
t . LastDoer = doerName
t . lock . Unlock ( )
2022-10-17 02:29:26 +03:00
if err := system_model . CreateNotice ( ctx , system_model . NoticeTask , config . FormatMessage ( translation . NewLocale ( "en-US" ) , t . Name , "cancelled" , doerName , message ) ) ; err != nil {
2020-05-17 02:31:38 +03:00
log . Error ( "CreateNotice: %v" , err )
}
return
}
2022-03-29 04:31:07 +03:00
t . lock . Lock ( )
t . Status = "finished"
t . LastMessage = ""
t . LastDoer = doerName
t . lock . Unlock ( )
2020-08-05 23:40:36 +03:00
if config . DoNoticeOnSuccess ( ) {
2022-10-17 02:29:26 +03:00
if err := system_model . CreateNotice ( ctx , system_model . NoticeTask , config . FormatMessage ( translation . NewLocale ( "en-US" ) , t . Name , "finished" , doerName ) ) ; err != nil {
2020-08-05 23:40:36 +03:00
log . Error ( "CreateNotice: %v" , err )
}
2020-05-17 02:31:38 +03:00
}
} )
}
// 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
2021-11-24 12:49:20 +03:00
func RegisterTask ( name string , config Config , fun func ( context . Context , * user_model . User , Config ) error ) error {
2020-05-17 02:31:38 +03:00
log . Debug ( "Registering task: %s" , name )
2022-02-07 10:43:53 +03:00
i18nKey := "admin.dashboard." + name
2024-02-15 00:48:45 +03:00
if value := translation . NewLocale ( "en-US" ) . TrString ( i18nKey ) ; value == i18nKey {
2022-02-07 10:43:53 +03:00
return fmt . Errorf ( "translation is missing for task %q, please add translation for %q" , name , i18nKey )
}
2020-05-17 02:31:38 +03:00
_ , 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
2023-07-24 07:13:41 +03:00
if err := addTaskToScheduler ( task ) ; err != nil {
2020-05-17 02:31:38 +03:00
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
2021-11-24 12:49:20 +03:00
func RegisterTaskFatal ( name string , config Config , fun func ( context . Context , * user_model . User , Config ) error ) {
2020-05-17 02:31:38 +03:00
if err := RegisterTask ( name , config , fun ) ; err != nil {
log . Fatal ( "Unable to register cron task %s Error: %v" , name , err )
}
}
2023-07-24 07:13:41 +03:00
func addTaskToScheduler ( task * Task ) error {
tags := [ ] string { task . Name , task . config . GetSchedule ( ) } // name and schedule can't be get from job, so we add them as tag
if scheduleHasSeconds ( task . config . GetSchedule ( ) ) {
scheduler = scheduler . CronWithSeconds ( task . config . GetSchedule ( ) )
} else {
scheduler = scheduler . Cron ( task . config . GetSchedule ( ) )
}
if _ , err := scheduler . Tag ( tags ... ) . Do ( task . Run ) ; err != nil {
log . Error ( "Unable to register cron task with name: %s Error: %v" , task . Name , err )
return err
}
return nil
}
func scheduleHasSeconds ( schedule string ) bool {
return len ( strings . Fields ( schedule ) ) >= 6
}