From 90e0a402c1185827fd3d5352ca915c531ce1e7b1 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Tue, 29 Mar 2022 02:31:07 +0100
Subject: [PATCH] Show last cron messages on monitor page (#19223)

As discussed on #19221 we should store the results of the last task message on the
crontask and show them on the monitor page.

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 options/locale/locale_en-US.ini |  1 +
 services/cron/cron.go           | 36 +++++++++++++++++-------
 services/cron/setting.go        | 19 ++++++-------
 services/cron/tasks.go          | 50 ++++++++++++++++++++++++---------
 templates/admin/cron.tmpl       | 35 +++++++++++++++++++++++
 templates/admin/monitor.tmpl    | 35 +----------------------
 6 files changed, 109 insertions(+), 67 deletions(-)
 create mode 100644 templates/admin/cron.tmpl

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 5008cd2bab..0a4abde408 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2816,6 +2816,7 @@ monitor.process = Running Processes
 monitor.desc = Description
 monitor.start = Start Time
 monitor.execute_time = Execution Time
+monitor.last_execution_result = Result
 monitor.process.cancel = Cancel process
 monitor.process.cancel_desc =  Cancelling a process may cause data loss
 monitor.process.cancel_notices =  Cancel: <strong>%s</strong>?
diff --git a/services/cron/cron.go b/services/cron/cron.go
index 19f703caf1..9fe90d4230 100644
--- a/services/cron/cron.go
+++ b/services/cron/cron.go
@@ -47,11 +47,23 @@ func NewContext() {
 
 // TaskTableRow represents a task row in the tasks table
 type TaskTableRow struct {
-	Name      string
-	Spec      string
-	Next      time.Time
-	Prev      time.Time
-	ExecTimes int64
+	Name        string
+	Spec        string
+	Next        time.Time
+	Prev        time.Time
+	Status      string
+	LastMessage string
+	LastDoer    string
+	ExecTimes   int64
+	task        *Task
+}
+
+func (t *TaskTableRow) FormatLastMessage(locale string) string {
+	if t.Status == "finished" {
+		return t.task.GetConfig().FormatMessage(locale, t.Name, t.Status, t.LastDoer)
+	}
+
+	return t.task.GetConfig().FormatMessage(locale, t.Name, t.Status, t.LastDoer, t.LastMessage)
 }
 
 // TaskTable represents a table of tasks
@@ -80,11 +92,15 @@ func ListTasks() TaskTable {
 		}
 		task.lock.Lock()
 		tTable = append(tTable, &TaskTableRow{
-			Name:      task.Name,
-			Spec:      spec,
-			Next:      next,
-			Prev:      prev,
-			ExecTimes: task.ExecTimes,
+			Name:        task.Name,
+			Spec:        spec,
+			Next:        next,
+			Prev:        prev,
+			ExecTimes:   task.ExecTimes,
+			LastMessage: task.LastMessage,
+			Status:      task.Status,
+			LastDoer:    task.LastDoer,
+			task:        task,
 		})
 		task.lock.Unlock()
 	}
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 4ccb7c7ce3..f0683393ff 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -7,8 +7,6 @@ package cron
 import (
 	"time"
 
-	user_model "code.gitea.io/gitea/models/user"
-
 	"github.com/unknwon/i18n"
 )
 
@@ -17,7 +15,7 @@ type Config interface {
 	IsEnabled() bool
 	DoRunAtStart() bool
 	GetSchedule() string
-	FormatMessage(name, status string, doer *user_model.User, args ...interface{}) string
+	FormatMessage(locale, name, status, doer string, args ...interface{}) string
 	DoNoticeOnSuccess() bool
 }
 
@@ -70,19 +68,20 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
 }
 
 // FormatMessage returns a message for the task
-func (b *BaseConfig) FormatMessage(name, status string, doer *user_model.User, args ...interface{}) string {
+// Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
+func (b *BaseConfig) FormatMessage(locale, name, status, doer string, 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, i18n.Tr(locale, "admin.dashboard."+name))
+	if doer == "" {
 		realArgs = append(realArgs, "(Cron)")
 	} else {
-		realArgs = append(realArgs, doer.Name)
+		realArgs = append(realArgs, doer)
 	}
 	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...)
+	if doer == "" {
+		return i18n.Tr(locale, "admin.dashboard.cron."+status, realArgs...)
 	}
-	return i18n.Tr("en-US", "admin.dashboard.task."+status, realArgs...)
+	return i18n.Tr(locale, "admin.dashboard.task."+status, realArgs...)
 }
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
index 070fb6e9e1..2252ad21e2 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -29,11 +29,14 @@ var (
 
 // Task represents a Cron task
 type Task struct {
-	lock      sync.Mutex
-	Name      string
-	config    Config
-	fun       func(context.Context, *user_model.User, Config) error
-	ExecTimes int64
+	lock        sync.Mutex
+	Name        string
+	config      Config
+	fun         func(context.Context, *user_model.User, Config) error
+	Status      string
+	LastMessage string
+	LastDoer    string
+	ExecTimes   int64
 }
 
 // DoRunAtStart returns if this task should run at the start
@@ -86,24 +89,45 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) {
 	}()
 	graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
 		pm := process.GetManager()
-		ctx, _, finished := pm.AddContext(baseCtx, config.FormatMessage(t.Name, "process", doer))
+		doerName := ""
+		if doer != nil && doer.ID != -1 {
+			doerName = doer.Name
+		}
+
+		ctx, _, finished := pm.AddContext(baseCtx, config.FormatMessage("en-US", t.Name, "process", doerName))
 		defer finished()
 
 		if err := t.fun(ctx, doer, config); err != nil {
+			var message string
+			var status string
 			if db.IsErrCancelled(err) {
-				message := err.(db.ErrCancelled).Message
-				if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil {
-					log.Error("CreateNotice: %v", err)
-				}
-				return
+				status = "cancelled"
+				message = err.(db.ErrCancelled).Message
+			} else {
+				status = "error"
+				message = err.Error()
 			}
-			if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil {
+
+			t.lock.Lock()
+			t.LastMessage = message
+			t.Status = status
+			t.LastDoer = doerName
+			t.lock.Unlock()
+
+			if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage("en-US", t.Name, "cancelled", doerName, message)); err != nil {
 				log.Error("CreateNotice: %v", err)
 			}
 			return
 		}
+
+		t.lock.Lock()
+		t.Status = "finished"
+		t.LastMessage = ""
+		t.LastDoer = doerName
+		t.lock.Unlock()
+
 		if config.DoNoticeOnSuccess() {
-			if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); err != nil {
+			if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage("en-US", t.Name, "finished", doerName)); err != nil {
 				log.Error("CreateNotice: %v", err)
 			}
 		}
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl
new file mode 100644
index 0000000000..30277177ed
--- /dev/null
+++ b/templates/admin/cron.tmpl
@@ -0,0 +1,35 @@
+<h4 class="ui top attached header">
+	{{.i18n.Tr "admin.monitor.cron"}}
+</h4>
+<div class="ui attached table segment">
+	<form method="post" action="{{AppSubUrl}}/admin">
+		<table class="ui very basic striped table">
+			<thead>
+				<tr>
+					<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>
+					<th>{{.i18n.Tr "admin.monitor.last_execution_result"}}</th>
+				</tr>
+			</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"}}</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>
+						<td {{if ne .Status ""}}class="tooltip" data-content="{{.FormatLastMessage $.i18n.Language}}"{{end}} >{{if eq .Status "" }}—{{else if eq .Status "finished"}}{{svg "octicon-check" 16}}{{else}}{{svg "octicon-x" 16}}{{end}}</td>
+					</tr>
+				{{end}}
+			</tbody>
+		</table>
+		<input type="hidden" name="from" value="monitor"/>
+		{{.CsrfTokenHtml}}
+	</form>
+</div>
diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl
index a35b587bd5..443159f8ce 100644
--- a/templates/admin/monitor.tmpl
+++ b/templates/admin/monitor.tmpl
@@ -3,40 +3,7 @@
 	{{template "admin/navbar" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
-		<h4 class="ui top attached header">
-			{{.i18n.Tr "admin.monitor.cron"}}
-		</h4>
-		<div class="ui attached table segment">
-			<form method="post" action="{{AppSubUrl}}/admin">
-				<table class="ui very basic striped table">
-					<thead>
-						<tr>
-							<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>
-					</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"}}</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>
-				<input type="hidden" name="from" value="monitor"/>
-				{{.CsrfTokenHtml}}
-			</form>
-		</div>
-
+		{{template "admin/cron" .}}
 		<h4 class="ui top attached header">
 			{{.i18n.Tr "admin.monitor.queues"}}
 		</h4>