mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Use globally shared HTMLRender (#24436)
The old `HTMLRender` is not ideal. 1. It shouldn't be initialized multiple times, it consumes a lot of memory and is slow. 2. It shouldn't depend on short-lived requests, the `WatchLocalChanges` needs a long-running context. 3. It doesn't make sense to use FuncsMap slice. HTMLRender was designed to only work for GItea's specialized 400+ templates, so it's good to make it a global shared instance.
This commit is contained in:
		| @@ -677,7 +677,7 @@ func getCsrfOpts() CsrfOptions { | |||||||
|  |  | ||||||
| // Contexter initializes a classic context for a request. | // Contexter initializes a classic context for a request. | ||||||
| func Contexter(ctx context.Context) func(next http.Handler) http.Handler { | func Contexter(ctx context.Context) func(next http.Handler) http.Handler { | ||||||
| 	_, rnd := templates.HTMLRenderer(ctx) | 	rnd := templates.HTMLRenderer() | ||||||
| 	csrfOpts := getCsrfOpts() | 	csrfOpts := getCsrfOpts() | ||||||
| 	if !setting.IsProd { | 	if !setting.IsProd { | ||||||
| 		CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose | 		CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) { | |||||||
|  |  | ||||||
| // PackageContexter initializes a package context for a request. | // PackageContexter initializes a package context for a request. | ||||||
| func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler { | func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler { | ||||||
| 	_, rnd := templates.HTMLRenderer(ctx) | 	rnd := templates.HTMLRenderer() | ||||||
| 	return func(next http.Handler) http.Handler { | 	return func(next http.Handler) http.Handler { | ||||||
| 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | ||||||
| 			ctx := Context{ | 			ctx := Context{ | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import ( | |||||||
| 	"html" | 	"html" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"regexp" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -26,12 +25,9 @@ import ( | |||||||
| 	"code.gitea.io/gitea/services/gitdiff" | 	"code.gitea.io/gitea/services/gitdiff" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Used from static.go && dynamic.go |  | ||||||
| var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`) |  | ||||||
|  |  | ||||||
| // NewFuncMap returns functions for injecting to templates | // NewFuncMap returns functions for injecting to templates | ||||||
| func NewFuncMap() []template.FuncMap { | func NewFuncMap() template.FuncMap { | ||||||
| 	return []template.FuncMap{map[string]interface{}{ | 	return map[string]interface{}{ | ||||||
| 		"DumpVar": dumpVar, | 		"DumpVar": dumpVar, | ||||||
|  |  | ||||||
| 		// ----------------------------------------------------------------- | 		// ----------------------------------------------------------------- | ||||||
| @@ -192,7 +188,7 @@ func NewFuncMap() []template.FuncMap { | |||||||
|  |  | ||||||
| 		"FilenameIsImage": FilenameIsImage, | 		"FilenameIsImage": FilenameIsImage, | ||||||
| 		"TabSizeClass":    TabSizeClass, | 		"TabSizeClass":    TabSizeClass, | ||||||
| 	}} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Safe render raw as HTML | // Safe render raw as HTML | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ package templates | |||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| @@ -15,24 +14,29 @@ import ( | |||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"sync/atomic" | 	"sync/atomic" | ||||||
| 	texttemplate "text/template" | 	texttemplate "text/template" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/assetfs" | 	"code.gitea.io/gitea/modules/assetfs" | ||||||
|  | 	"code.gitea.io/gitea/modules/graceful" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/templates/scopedtmpl" | 	"code.gitea.io/gitea/modules/templates/scopedtmpl" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var rendererKey interface{} = "templatesHtmlRenderer" |  | ||||||
|  |  | ||||||
| type TemplateExecutor scopedtmpl.TemplateExecutor | type TemplateExecutor scopedtmpl.TemplateExecutor | ||||||
|  |  | ||||||
| type HTMLRender struct { | type HTMLRender struct { | ||||||
| 	templates atomic.Pointer[scopedtmpl.ScopedTemplate] | 	templates atomic.Pointer[scopedtmpl.ScopedTemplate] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	htmlRender     *HTMLRender | ||||||
|  | 	htmlRenderOnce sync.Once | ||||||
|  | ) | ||||||
|  |  | ||||||
| var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") | var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") | ||||||
|  |  | ||||||
| func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error { | func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error { | ||||||
| @@ -55,14 +59,14 @@ func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) { | |||||||
| 		return nil, ErrTemplateNotInitialized | 		return nil, ErrTemplateNotInitialized | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return tmpls.Executor(name, NewFuncMap()[0]) | 	return tmpls.Executor(name, NewFuncMap()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (h *HTMLRender) CompileTemplates() error { | func (h *HTMLRender) CompileTemplates() error { | ||||||
| 	assets := AssetFS() | 	assets := AssetFS() | ||||||
| 	extSuffix := ".tmpl" | 	extSuffix := ".tmpl" | ||||||
| 	tmpls := scopedtmpl.NewScopedTemplate() | 	tmpls := scopedtmpl.NewScopedTemplate() | ||||||
| 	tmpls.Funcs(NewFuncMap()[0]) | 	tmpls.Funcs(NewFuncMap()) | ||||||
| 	files, err := ListWebTemplateAssetNames(assets) | 	files, err := ListWebTemplateAssetNames(assets) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil | 		return nil | ||||||
| @@ -86,20 +90,21 @@ func (h *HTMLRender) CompileTemplates() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use | // HTMLRenderer init once and returns the globally shared html renderer | ||||||
| func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) { | func HTMLRenderer() *HTMLRender { | ||||||
| 	if renderer, ok := ctx.Value(rendererKey).(*HTMLRender); ok { | 	htmlRenderOnce.Do(initHTMLRenderer) | ||||||
| 		return ctx, renderer | 	return htmlRender | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func initHTMLRenderer() { | ||||||
| 	rendererType := "static" | 	rendererType := "static" | ||||||
| 	if !setting.IsProd { | 	if !setting.IsProd { | ||||||
| 		rendererType = "auto-reloading" | 		rendererType = "auto-reloading" | ||||||
| 	} | 	} | ||||||
| 	log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer") | 	log.Debug("Creating %s HTML Renderer", rendererType) | ||||||
|  |  | ||||||
| 	renderer := &HTMLRender{} | 	htmlRender = &HTMLRender{} | ||||||
| 	if err := renderer.CompileTemplates(); err != nil { | 	if err := htmlRender.CompileTemplates(); err != nil { | ||||||
| 		p := &templateErrorPrettier{assets: AssetFS()} | 		p := &templateErrorPrettier{assets: AssetFS()} | ||||||
| 		wrapFatal(p.handleFuncNotDefinedError(err)) | 		wrapFatal(p.handleFuncNotDefinedError(err)) | ||||||
| 		wrapFatal(p.handleUnexpectedOperandError(err)) | 		wrapFatal(p.handleUnexpectedOperandError(err)) | ||||||
| @@ -107,14 +112,14 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) { | |||||||
| 		wrapFatal(p.handleGenericTemplateError(err)) | 		wrapFatal(p.handleGenericTemplateError(err)) | ||||||
| 		log.Fatal("HTMLRenderer CompileTemplates error: %v", err) | 		log.Fatal("HTMLRenderer CompileTemplates error: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !setting.IsProd { | 	if !setting.IsProd { | ||||||
| 		go AssetFS().WatchLocalChanges(ctx, func() { | 		go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() { | ||||||
| 			if err := renderer.CompileTemplates(); err != nil { | 			if err := htmlRender.CompileTemplates(); err != nil { | ||||||
| 				log.Error("Template error: %v\n%s", err, log.Stack(2)) | 				log.Error("Template error: %v\n%s", err, log.Stack(2)) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	return context.WithValue(ctx, rendererKey, renderer), renderer |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func wrapFatal(msg string) { | func wrapFatal(msg string) { | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package templates | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"html/template" | 	"html/template" | ||||||
|  | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	texttmpl "text/template" | 	texttmpl "text/template" | ||||||
|  |  | ||||||
| @@ -14,6 +15,8 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`) | ||||||
|  |  | ||||||
| // mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject | // mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject | ||||||
| func mailSubjectTextFuncMap() texttmpl.FuncMap { | func mailSubjectTextFuncMap() texttmpl.FuncMap { | ||||||
| 	return texttmpl.FuncMap{ | 	return texttmpl.FuncMap{ | ||||||
| @@ -55,9 +58,7 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) { | |||||||
| 	bodyTemplates := template.New("") | 	bodyTemplates := template.New("") | ||||||
|  |  | ||||||
| 	subjectTemplates.Funcs(mailSubjectTextFuncMap()) | 	subjectTemplates.Funcs(mailSubjectTextFuncMap()) | ||||||
| 	for _, funcs := range NewFuncMap() { | 	bodyTemplates.Funcs(NewFuncMap()) | ||||||
| 		bodyTemplates.Funcs(funcs) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	assetFS := AssetFS() | 	assetFS := AssetFS() | ||||||
| 	refreshTemplates := func() { | 	refreshTemplates := func() { | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ const ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) { | func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) { | ||||||
| 	_, rnd := templates.HTMLRenderer(req.Context()) | 	rnd := templates.HTMLRenderer() | ||||||
| 	resp := httptest.NewRecorder() | 	resp := httptest.NewRecorder() | ||||||
| 	c := &context.Context{ | 	c := &context.Context{ | ||||||
| 		Req:    req, | 		Req:    req, | ||||||
|   | |||||||
| @@ -175,7 +175,7 @@ func GlobalInitInstalled(ctx context.Context) { | |||||||
|  |  | ||||||
| // NormalRoutes represents non install routes | // NormalRoutes represents non install routes | ||||||
| func NormalRoutes(ctx context.Context) *web.Route { | func NormalRoutes(ctx context.Context) *web.Route { | ||||||
| 	ctx, _ = templates.HTMLRenderer(ctx) | 	_ = templates.HTMLRenderer() | ||||||
| 	r := web.NewRoute() | 	r := web.NewRoute() | ||||||
| 	r.Use(common.ProtocolMiddlewares()...) | 	r.Use(common.ProtocolMiddlewares()...) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -55,7 +55,7 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) { | |||||||
|  |  | ||||||
| // Init prepare for rendering installation page | // Init prepare for rendering installation page | ||||||
| func Init(ctx goctx.Context) func(next http.Handler) http.Handler { | func Init(ctx goctx.Context) func(next http.Handler) http.Handler { | ||||||
| 	_, rnd := templates.HTMLRenderer(ctx) | 	rnd := templates.HTMLRenderer() | ||||||
| 	dbTypeNames := getSupportedDbTypeNames() | 	dbTypeNames := getSupportedDbTypeNames() | ||||||
| 	return func(next http.Handler) http.Handler { | 	return func(next http.Handler) http.Handler { | ||||||
| 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler { | |||||||
| 					if !setting.IsProd { | 					if !setting.IsProd { | ||||||
| 						store["ErrorMsg"] = combinedErr | 						store["ErrorMsg"] = combinedErr | ||||||
| 					} | 					} | ||||||
| 					_, rnd := templates.HTMLRenderer(ctx) | 					rnd := templates.HTMLRenderer() | ||||||
| 					err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store)) | 					err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store)) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						log.Error("%v", err) | 						log.Error("%v", err) | ||||||
|   | |||||||
| @@ -120,7 +120,7 @@ func (d *dataStore) GetData() map[string]interface{} { | |||||||
| // RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so. | // RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so. | ||||||
| // This error will be created with the gitea 500 page. | // This error will be created with the gitea 500 page. | ||||||
| func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler { | func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler { | ||||||
| 	_, rnd := templates.HTMLRenderer(ctx) | 	rnd := templates.HTMLRenderer() | ||||||
| 	return func(next http.Handler) http.Handler { | 	return func(next http.Handler) http.Handler { | ||||||
| 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||||
| 			defer func() { | 			defer func() { | ||||||
|   | |||||||
| @@ -114,7 +114,8 @@ func Routes(ctx gocontext.Context) *web.Route { | |||||||
| 	routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) | 	routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) | ||||||
| 	routes.RouteMethods("/favicon.ico", "GET, HEAD", misc.StaticRedirect("/assets/img/favicon.png")) | 	routes.RouteMethods("/favicon.ico", "GET, HEAD", misc.StaticRedirect("/assets/img/favicon.png")) | ||||||
|  |  | ||||||
| 	ctx, _ = templates.HTMLRenderer(ctx) | 	_ = templates.HTMLRenderer() | ||||||
|  |  | ||||||
| 	common := []any{ | 	common := []any{ | ||||||
| 		common.Sessioner(), | 		common.Sessioner(), | ||||||
| 		RecoveryWith500Page(ctx), | 		RecoveryWith500Page(ctx), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user