diff --git a/config/config.go b/config/config.go index ef34243..a9205f3 100644 --- a/config/config.go +++ b/config/config.go @@ -100,7 +100,10 @@ type AmConfig struct { SessionExpire string `yaml:"sessionExpire"` UserAgreementResource string `yaml:"userAgreementResource"` PolicyResource string `yaml:"policyResource"` + FrameTemplate string `yaml:"frameTemplate"` FooterTemplate string `yaml:"footerTemplate"` + TopMenuId string `yaml:"topMenuId"` + FixedMenuId string `yaml:"fixedMenuId"` DefaultCommunityLogo string `yaml:"defaultCommunityLogo"` DefaultUserPhoto string `yaml:"defaultUserPhoto"` WelcomeTitle string `yaml:"welcomeTitle"` @@ -148,6 +151,9 @@ type AmConfig struct { Prioritize string `yaml:"prioritize"` } `yaml:"countryList"` VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"` + PanicRecovery struct { + StackDataSize string `yaml:"stackDataSize"` + } `yaml:"panicRecovery"` } `yaml:"rendering"` Resources struct { ViewTemplateDir string `yaml:"viewTemplateDir"` @@ -168,9 +174,11 @@ type AmConfig struct { Tuning struct { WorkerTasks int `yaml:"workerTasks"` Timeouts struct { - HttpRead int `yaml:"httpRead"` - HttpWrite int `yaml:"httpWrite"` - HttpIdle int `yaml:"httpIdle"` + HttpRead int `yaml:"httpRead"` + HttpWrite int `yaml:"httpWrite"` + HttpIdle int `yaml:"httpIdle"` + PageExecute int `yaml:"pageExecute"` + PageRender int `yaml:"pageRender"` } `yaml:"timeouts"` Queues struct { AuditWrites int `yaml:"auditWrites"` @@ -207,22 +215,23 @@ func (c *AmConfig) ExPath(path string) string { // AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig and CommandLine. type AmConfigComputed struct { - DebugMode bool // are we in debug mode? - LogLevel string // the logging level - Listen string // listen address - DatabaseDriver string // name of database driver - DatabaseHost string // hostname for database - DatabaseUser string // user name for database - DatabasePassword string // password for database - DatabaseName string // database name - MailHost string // SMTP host - MailPort int // SMTP port - MailTLS string // SMTP TLS setting - MailAuthType string // SMTP auth type - MailUser string // SMTP user name - MailPassword string // SMTP password - UploadMaxSize int32 // maximum upload size in bytes - UploadNoCompress map[string]bool // which upload types are not compressed? + DebugMode bool // are we in debug mode? + LogLevel string // the logging level + Listen string // listen address + DatabaseDriver string // name of database driver + DatabaseHost string // hostname for database + DatabaseUser string // user name for database + DatabasePassword string // password for database + DatabaseName string // database name + MailHost string // SMTP host + MailPort int // SMTP port + MailTLS string // SMTP TLS setting + MailAuthType string // SMTP auth type + MailUser string // SMTP user name + MailPassword string // SMTP password + PanicRecoveryStack int32 // stack size for panic recovery + UploadMaxSize int32 // maximum upload size in bytes + UploadNoCompress map[string]bool // which upload types are not compressed? } //go:embed default.yaml @@ -403,7 +412,12 @@ func SetupConfig() { GlobalComputedConfig.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType) GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User) GlobalComputedConfig.MailPassword = util.IIF(CommandLine.MailPassword != "", CommandLine.MailPassword, GlobalConfig.Email.Password) - tmp, err := humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize) + tmp, err := humanize.ParseBytes(GlobalConfig.Rendering.PanicRecovery.StackDataSize) + if err != nil { + panic(err.Error()) + } + GlobalComputedConfig.PanicRecoveryStack = int32(tmp) + tmp, err = humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize) if err != nil { panic(err.Error()) } diff --git a/config/default.yaml b/config/default.yaml index 416b535..2c4260e 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -25,7 +25,10 @@ site: sessionExpire: "3h" userAgreementResource: "useragreement.html" policyResource: "policy.html" + frameTemplate: "frame.jet" footerTemplate: "footer.jet" + topMenuId: "top" + fixedMenuId: "fixed" defaultCommunityLogo: "/img/builtin/default-community.jpg" defaultUserPhoto: "/img/builtin/no-user.png" welcomeTitle: "Welcome to Amsterdam" @@ -72,6 +75,8 @@ rendering: countryList: prioritize: US veniceCompatibleImageURLs: false + panicRecovery: + stackDataSize: "4 KiB" resources: viewTemplateDir: "" dialogTemplateDir: "" @@ -95,6 +100,8 @@ tuning: httpRead: 30 httpWrite: 30 httpIdle: 120 + pageExecute: 15 + pageRender: 15 queues: auditWrites: 16 contextRecycle: 16 diff --git a/main.go b/main.go index 464735c..650cc9a 100644 --- a/main.go +++ b/main.go @@ -50,7 +50,9 @@ func setupEcho() *echo.Echo { e.Renderer = &ui.TemplateRenderer{} e.HTTPErrorHandler = AmErrorHandler if !config.CommandLine.DebugPanic { - e.Use(middleware.RecoverWithConfig(middleware.DefaultRecoverConfig)) + e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: int(config.GlobalComputedConfig.PanicRecoveryStack), + })) } else { log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!") } diff --git a/ui/render_wrap.go b/ui/render_wrap.go index 3790324..5497bf9 100644 --- a/ui/render_wrap.go +++ b/ui/render_wrap.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -44,6 +44,51 @@ func (e *panicRecoveryErr) Unwrap() error { return e.Err } +// doFrameRender renders the outer frame template with an inner template. +func doFrameRender(ctxt *echo.Context, amctxt AmContext, statusCode int, innerPage string) error { + if amctxt.FrameTitle() == "" { + log.Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath()) + amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>") + } + amctxt.VarMap().Set("__innerPage", innerPage) + menus := make([]*MenuDefinition, 2) + switch amctxt.LeftMenu() { + case "top": + menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId) + case "community": + comm := amctxt.CurrentCommunity() + if comm != nil { + md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm) + if err != nil { + return err + } + menus[0] = md + } else { + menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId) + } + default: + return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu()) + } + menus[1] = AmMenu(config.GlobalConfig.Site.FixedMenuId) + amctxt.VarMap().Set("__leftMenus", menus) + ad, err := database.AmGetRandomAd(ctxt.Request().Context()) + if err != nil { + ad = &database.Advert{ + AdId: -1, + ImagePath: "", + PathStyle: -1, + Caption: nil, + LinkURL: nil, + } + } + amctxt.VarMap().Set("__bannerad", ad) + amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode) + if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil { + amctxt.VarMap().Set("__suppressLogin", true) + } + return ctxt.Render(statusCode, config.GlobalConfig.Site.FrameTemplate, amctxt) +} + /* AmSendPageData sends page data to the output based on the command string. * Parameters: * ctxt - The Echo context from the request. @@ -65,20 +110,22 @@ func (e *panicRecoveryErr) Unwrap() error { */ func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data any) error { // Enable panic recovery. - defer func() { - if r := recover(); r != nil { - if r == http.ErrAbortHandler { - panic(r) + if !config.CommandLine.DebugPanic { + defer func() { + if r := recover(); r != nil { + if r == http.ErrAbortHandler { + panic(r) + } + tmperr, ok := r.(error) + if !ok { + tmperr = fmt.Errorf("%v", r) + } + stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack) + length := runtime.Stack(stack, false) + log.Errorf("[Panic Recovery in SendData Phase] %s %s", tmperr.Error(), stack[:length]) } - tmperr, ok := r.(error) - if !ok { - tmperr = fmt.Errorf("%v", r) - } - stack := make([]byte, 4<<10) - length := runtime.Stack(stack, false) - log.Errorf("[Panic Recovery in SendData Phase] %s %s", tmperr.Error(), stack[:length]) - } - }() + }() + } // Preprocess certain commands into different ones. httprc := http.StatusOK @@ -132,7 +179,7 @@ func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data a // Process commands. oldreq := ctxt.Request() - ctx, cancel := context.WithTimeout(oldreq.Context(), 15*time.Second) + ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageRender)*time.Second) defer cancel() ctxt.SetRequest(oldreq.WithContext(ctx)) defer ctxt.SetRequest(oldreq) @@ -151,47 +198,7 @@ func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data a case "template": err = ctxt.Render(httprc, data.(string), amctxt) case "framed": - if amctxt.FrameTitle() == "" { - log.Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath()) - amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>") - } - amctxt.VarMap().Set("__innerPage", data) - menus := make([]*MenuDefinition, 2) - switch amctxt.LeftMenu() { - case "top": - menus[0] = AmMenu("top") - case "community": - comm := amctxt.CurrentCommunity() - if comm != nil { - md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm) - if err != nil { - return err - } - menus[0] = md - } else { - menus[0] = AmMenu("top") - } - default: - return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu()) - } - menus[1] = AmMenu("fixed") - amctxt.VarMap().Set("__leftMenus", menus) - ad, err := database.AmGetRandomAd(ctxt.Request().Context()) - if err != nil { - ad = &database.Advert{ - AdId: -1, - ImagePath: "", - PathStyle: -1, - Caption: nil, - LinkURL: nil, - } - } - amctxt.VarMap().Set("__bannerad", ad) - amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode) - if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil { - amctxt.VarMap().Set("__suppressLogin", true) - } - err = ctxt.Render(httprc, "frame.jet", amctxt) + err = doFrameRender(ctxt, amctxt, httprc, data.(string)) default: err = fmt.Errorf("AmSendPageData(): unknown rendering type: %s", command) } @@ -209,23 +216,25 @@ type AmPageFunc func(AmContext) (string, any) // callWrappedPageFunc calls the specified page functon inside a wrapper that handles timeouts and panic recovery. func callWrappedPageFunc(f AmPageFunc, ctxt *echo.Context, amctxt AmContext) (command string, arg any) { - defer func() { - if r := recover(); r != nil { - if r == http.ErrAbortHandler { - panic(r) + if !config.CommandLine.DebugPanic { + defer func() { + if r := recover(); r != nil { + if r == http.ErrAbortHandler { + panic(r) + } + tmperr, ok := r.(error) + if !ok { + tmperr = fmt.Errorf("%v", r) + } + stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack) + length := runtime.Stack(stack, false) + arg = &panicRecoveryErr{Phase: "PageFunc", Err: tmperr, Stack: stack[:length]} + command = "error" } - tmperr, ok := r.(error) - if !ok { - tmperr = fmt.Errorf("%v", r) - } - stack := make([]byte, 4<<10) - length := runtime.Stack(stack, false) - arg = &panicRecoveryErr{Phase: "PageFunc", Err: tmperr, Stack: stack[:length]} - command = "error" - } - }() + }() + } oldreq := ctxt.Request() - ctx, cancel := context.WithTimeout(oldreq.Context(), 15*time.Second) + ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageExecute)*time.Second) defer cancel() ctxt.SetRequest(oldreq.WithContext(ctx)) defer ctxt.SetRequest(oldreq)