From ec2a01d1e20c0d33c1ea7e362a7dfd5b653dd15f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 22 May 2023 09:38:38 +0800
Subject: [PATCH] Fix regression: access log template, gitea manager cli
 command (#24838)

Close #24836

![image](https://github.com/go-gitea/gitea/assets/2114189/95b025d2-f25f-4246-a08a-fe44ecb787a9)

![image](https://github.com/go-gitea/gitea/assets/2114189/c3afe1fa-2a23-420d-a016-3b67dcd04cd5)
---
 modules/context/access_log.go    |  2 +-
 modules/context/response.go      |  5 +++++
 modules/graceful/manager_unix.go |  4 ++++
 modules/graceful/restart_unix.go |  4 +++-
 modules/private/actions.go       |  1 -
 modules/private/manager.go       | 20 ++++++++++----------
 modules/private/request.go       | 30 +++++++++++-------------------
 modules/private/restore_repo.go  |  2 +-
 routers/private/restore_repo.go  |  2 +-
 9 files changed, 36 insertions(+), 34 deletions(-)

diff --git a/modules/context/access_log.go b/modules/context/access_log.go
index 9b649a6a01..373574ba14 100644
--- a/modules/context/access_log.go
+++ b/modules/context/access_log.go
@@ -92,7 +92,7 @@ func AccessLogger() func(http.Handler) http.Handler {
 				RequestID: &requestID,
 			})
 			if err != nil {
-				log.Error("Could not set up chi access logger: %v", err.Error())
+				log.Error("Could not execute access logger template: %v", err.Error())
 			}
 
 			logger.Info("%s", buf.String())
diff --git a/modules/context/response.go b/modules/context/response.go
index ca52ea137d..8708d77da0 100644
--- a/modules/context/response.go
+++ b/modules/context/response.go
@@ -13,6 +13,7 @@ type ResponseWriter interface {
 	http.Flusher
 	Status() int
 	Before(func(ResponseWriter))
+	Size() int // used by access logger template
 }
 
 var _ ResponseWriter = &Response{}
@@ -45,6 +46,10 @@ func (r *Response) Write(bs []byte) (int, error) {
 	return size, nil
 }
 
+func (r *Response) Size() int {
+	return r.written
+}
+
 // WriteHeader write status code
 func (r *Response) WriteHeader(statusCode int) {
 	if !r.beforeExecuted {
diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go
index d89f6fc725..f949abfd21 100644
--- a/modules/graceful/manager_unix.go
+++ b/modules/graceful/manager_unix.go
@@ -244,6 +244,10 @@ func (g *Manager) DoGracefulRestart() {
 				log.Error("Error whilst forking from PID: %d : %v", os.Getpid(), err)
 			}
 		}
+		// doFork calls RestartProcess which starts a new Gitea process, so this parent process needs to exit
+		// Otherwise some resources (eg: leveldb lock) will be held by this parent process and the new process will fail to start
+		log.Info("PID: %d. Shutting down after forking ...", os.Getpid())
+		g.doShutdown()
 	} else {
 		log.Info("PID: %d. Not set restartable. Shutting down...", os.Getpid())
 		g.notify(stoppingMsg)
diff --git a/modules/graceful/restart_unix.go b/modules/graceful/restart_unix.go
index a7c2b8288a..98d5c5cc20 100644
--- a/modules/graceful/restart_unix.go
+++ b/modules/graceful/restart_unix.go
@@ -109,5 +109,7 @@ func RestartProcess() (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	return process.Pid, nil
+	processPid := process.Pid
+	_ = process.Release() // no wait, so release
+	return processPid, nil
 }
diff --git a/modules/private/actions.go b/modules/private/actions.go
index be24e16d3f..c924dac2cd 100644
--- a/modules/private/actions.go
+++ b/modules/private/actions.go
@@ -9,7 +9,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 )
 
-// Email structure holds a data for sending general emails
 type GenerateTokenRequest struct {
 	Scope string
 }
diff --git a/modules/private/manager.go b/modules/private/manager.go
index 3448f2e34c..a974c3ed05 100644
--- a/modules/private/manager.go
+++ b/modules/private/manager.go
@@ -19,14 +19,14 @@ import (
 func Shutdown(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/shutdown"
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Shutting down")
+	return requestJSONClientMsg(req, "Shutting down")
 }
 
 // Restart calls the internal restart function
 func Restart(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/restart"
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Restarting")
+	return requestJSONClientMsg(req, "Restarting")
 }
 
 // FlushOptions represents the options for the flush call
@@ -42,35 +42,35 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R
 	if timeout > 0 {
 		req.SetReadWriteTimeout(timeout + 10*time.Second)
 	}
-	return requestJSONUserMsg(req, "Flushed")
+	return requestJSONClientMsg(req, "Flushed")
 }
 
 // PauseLogging pauses logging
 func PauseLogging(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/pause-logging"
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Logging Paused")
+	return requestJSONClientMsg(req, "Logging Paused")
 }
 
 // ResumeLogging resumes logging
 func ResumeLogging(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/resume-logging"
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Logging Restarted")
+	return requestJSONClientMsg(req, "Logging Restarted")
 }
 
 // ReleaseReopenLogging releases and reopens logging files
 func ReleaseReopenLogging(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging"
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Logging Restarted")
+	return requestJSONClientMsg(req, "Logging Restarted")
 }
 
 // SetLogSQL sets database logging
 func SetLogSQL(ctx context.Context, on bool) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on)
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Log SQL setting set")
+	return requestJSONClientMsg(req, "Log SQL setting set")
 }
 
 // LoggerOptions represents the options for the add logger call
@@ -90,14 +90,14 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri
 		Mode:   mode,
 		Config: config,
 	})
-	return requestJSONUserMsg(req, "Added")
+	return requestJSONClientMsg(req, "Added")
 }
 
 // RemoveLogger removes a logger
 func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
 	req := newInternalRequest(ctx, reqURL, "POST")
-	return requestJSONUserMsg(req, "Removed")
+	return requestJSONClientMsg(req, "Removed")
 }
 
 // Processes return the current processes from this gitea instance
@@ -108,6 +108,6 @@ func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces,
 	callback := func(resp *http.Response, extra *ResponseExtra) {
 		_, extra.Error = io.Copy(out, resp.Body)
 	}
-	_, extra := requestJSONResp(req, &callback)
+	_, extra := requestJSONResp(req, &responseCallback{callback})
 	return extra
 }
diff --git a/modules/private/request.go b/modules/private/request.go
index 3eb8c92c1a..d3f99381a6 100644
--- a/modules/private/request.go
+++ b/modules/private/request.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"unicode"
 
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/json"
@@ -25,7 +24,9 @@ type ResponseExtra struct {
 	Error      error
 }
 
-type responseCallback func(resp *http.Response, extra *ResponseExtra)
+type responseCallback struct {
+	Callback func(resp *http.Response, extra *ResponseExtra)
+}
 
 func (re *ResponseExtra) HasError() bool {
 	return re.Error != nil
@@ -43,7 +44,7 @@ func (re responseError) Error() string {
 	return fmt.Sprintf("internal API error response, status=%d, err=%s", re.statusCode, re.errorString)
 }
 
-// requestJSONUserMsg sends a request to the gitea server and then parses the response.
+// requestJSONResp sends a request to the gitea server and then parses the response.
 // If the status code is not 2xx, or any error occurs, the ResponseExtra.Error field is guaranteed to be non-nil,
 // and the ResponseExtra.UserMsg field will be set to a message for the end user.
 //
@@ -89,10 +90,10 @@ func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra Respons
 		}
 		respText.Text = string(bs)
 		return res, extra
-	} else if callback, ok := v.(*responseCallback); ok {
+	} else if cb, ok := v.(*responseCallback); ok {
 		// pass the response to callback, and let the callback update the ResponseExtra
 		extra.StatusCode = resp.StatusCode
-		(*callback)(resp, &extra)
+		cb.Callback(resp, &extra)
 		return nil, extra
 	} else if err := json.NewDecoder(resp.Body).Decode(res); err != nil {
 		// decode the response into the given struct
@@ -114,22 +115,13 @@ func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra Respons
 	return res, extra
 }
 
-// requestJSONUserMsg sends a request to the gitea server and then parses the response as private.Response
-// If the request succeeds, the successMsg will be used as part of ResponseExtra.UserMsg.
-func requestJSONUserMsg(req *httplib.Request, successMsg string) ResponseExtra {
-	resp, extra := requestJSONResp(req, &Response{})
+// requestJSONClientMsg sends a request to the gitea server, server only responds text message status=200 with "success" body
+// If the request succeeds (200), the argument clientSuccessMsg will be used as ResponseExtra.UserMsg.
+func requestJSONClientMsg(req *httplib.Request, clientSuccessMsg string) ResponseExtra {
+	_, extra := requestJSONResp(req, &responseText{})
 	if extra.HasError() {
 		return extra
 	}
-	if resp.UserMsg == "" {
-		extra.UserMsg = successMsg // if UserMsg is empty, then use successMsg as userMsg
-	} else if successMsg != "" {
-		// else, now UserMsg is not empty, if successMsg is not empty, then append successMsg to UserMsg
-		if unicode.IsPunct(rune(extra.UserMsg[len(extra.UserMsg)-1])) {
-			extra.UserMsg = extra.UserMsg + " " + successMsg
-		} else {
-			extra.UserMsg = extra.UserMsg + ". " + successMsg
-		}
-	}
+	extra.UserMsg = clientSuccessMsg
 	return extra
 }
diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go
index 34d0f5d482..496209d3cb 100644
--- a/modules/private/restore_repo.go
+++ b/modules/private/restore_repo.go
@@ -32,5 +32,5 @@ func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units
 		Validation: validation,
 	})
 	req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout
-	return requestJSONUserMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName))
+	return requestJSONClientMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName))
 }
diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go
index 97ac9a3c5a..7efc22a3d9 100644
--- a/routers/private/restore_repo.go
+++ b/routers/private/restore_repo.go
@@ -48,6 +48,6 @@ func RestoreRepo(ctx *myCtx.PrivateContext) {
 			Err: err.Error(),
 		})
 	} else {
-		ctx.Status(http.StatusOK)
+		ctx.PlainText(http.StatusOK, "success")
 	}
 }