Files
aerc-fork-mirror/app/app.go
Robin Jarry 51fd25c0f1 reload: fix crash when reloading via IPC
When reloading the configuration with :reload, global variables in the
config package are reset to their startup values and then, the config is
parsed from disk. While the parsing is done, these variables are
temporarily in an inconsistent and possibly invalid state.

When commands are executed interactively from aerc, they are handled by
the main goroutine which also deals with UI rendering. No UI render will
be done while :reload is in progress.

However, the IPC socket handler runs in an independent goroutine. This
has the unfortunate side effect to let the UI goroutine to run while
config parsing is in progress and causes crashes:

[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6bb142]

goroutine 1 [running]:
git.sr.ht/~rjarry/aerc/lib/log.PanicHandler()
	lib/log/panic-logger.go:51 +0x6cf
panic({0xc1d960?, 0x134a6e0?})
	/usr/lib/go/src/runtime/panic.go:783 +0x132
git.sr.ht/~rjarry/aerc/config.(*StyleConf).getStyle(0xc00038b908?, 0x4206b7?)
	config/style.go:386 +0x42
git.sr.ht/~rjarry/aerc/config.StyleSet.Get({0x0, 0x0, 0x0, {0x0, 0x0, 0x0}}, 0x421a65?, 0x0)
	config/style.go:408 +0x8b
git.sr.ht/~rjarry/aerc/config.(*UIConfig).GetStyle(...)
	config/ui.go:379
git.sr.ht/~rjarry/aerc/lib/ui.(*TabStrip).Draw(0xc000314700, 0xc000192230)
	lib/ui/tab.go:378 +0x15b
git.sr.ht/~rjarry/aerc/lib/ui.(*Grid).Draw(0xc000186fc0, 0xc0002c25f0)
	lib/ui/grid.go:126 +0x28e
git.sr.ht/~rjarry/aerc/app.(*Aerc).Draw(0x14b9f00, 0xc0002c25f0)
	app/aerc.go:192 +0x1fe
git.sr.ht/~rjarry/aerc/lib/ui.Render()
	lib/ui/ui.go:155 +0x16b
main.main()
	main.go:310 +0x997

Make the reload operation safe by changing how config objects are
exposed and updated. Change all objects to be atomic pointers. Expose
public functions to access their value atomically. Only update their
value after a complete and successful config parse. This way the UI
thread will always have access to a valid configuration.

NB: The account configuration is not included in this change since it
cannot be reloaded.

Fixes: https://todo.sr.ht/~rjarry/aerc/319
Reported-by: Anachron <gith@cron.world>
Signed-off-by: Robin Jarry <robin@jarry.cc>
2025-09-23 14:02:37 +02:00

103 lines
4.2 KiB
Go

package app
import (
"context"
"time"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/crypto"
"git.sr.ht/~rjarry/aerc/lib/ipc"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~rjarry/go-opt/v2"
"github.com/ProtonMail/go-crypto/openpgp"
)
var aerc Aerc
func Init(
crypto crypto.Provider,
cmd func(string, *config.AccountConfig, *models.MessageInfo) error,
complete func(ctx context.Context, cmd string) ([]opt.Completion, string), history lib.History,
deferLoop chan struct{},
) {
aerc.Init(crypto, cmd, complete, history, deferLoop)
}
func Drawable() ui.DrawableInteractive { return &aerc }
func IPCHandler() ipc.Handler { return &aerc }
func Command(args []string) error { return aerc.Command(args) }
func HandleMessage(msg types.WorkerMessage) { aerc.HandleMessage(msg) }
func CloseBackends() error { return aerc.CloseBackends() }
func AddDialog(d ui.DrawableInteractive) { aerc.AddDialog(d) }
func CloseDialog() { aerc.CloseDialog() }
func HumanReadableBindings() []string {
return aerc.HumanReadableBindings()
}
func Account(name string) (*AccountView, error) { return aerc.Account(name) }
func AccountNames() []string { return aerc.AccountNames() }
func NextAccount() (*AccountView, error) { return aerc.NextAccount() }
func PrevAccount() (*AccountView, error) { return aerc.PrevAccount() }
func SelectedAccount() *AccountView { return aerc.SelectedAccount() }
func SelectedAccountUiConfig() *config.UIConfig { return aerc.SelectedAccountUiConfig() }
func NextTab() { aerc.NextTab() }
func PrevTab() { aerc.PrevTab() }
func PinTab() { aerc.PinTab() }
func UnpinTab() { aerc.UnpinTab() }
func MoveTab(i int, relative bool) { aerc.MoveTab(i, relative) }
func TabNames() []string { return aerc.TabNames() }
func GetTab(i int) *ui.Tab { return aerc.tabs.Get(i) }
func SelectTab(name string) bool { return aerc.SelectTab(name) }
func SelectPreviousTab() bool { return aerc.SelectPreviousTab() }
func SelectedTab() *ui.Tab { return aerc.SelectedTab() }
func SelectedTabContent() ui.Drawable { return aerc.SelectedTabContent() }
func SelectTabIndex(index int) bool { return aerc.SelectTabIndex(index) }
func SelectTabAtOffset(offset int) { aerc.SelectTabAtOffset(offset) }
func RemoveTab(tab ui.Drawable, closeContent bool) { aerc.RemoveTab(tab, closeContent) }
func NewTab(clickable ui.Drawable, name string) *ui.Tab {
return aerc.NewTab(clickable, name, false)
}
func NewBackgroundTab(clickable ui.Drawable, name string) *ui.Tab {
return aerc.NewTab(clickable, name, true)
}
func ReplaceTab(tabSrc ui.Drawable, tabTarget ui.Drawable, name string, closeSrc bool) {
aerc.ReplaceTab(tabSrc, tabTarget, name, closeSrc)
}
func UpdateStatus() { aerc.UpdateStatus() }
func PushPrompt(prompt *ExLine) { aerc.PushPrompt(prompt) }
func SetError(text string) { aerc.SetError(text) }
func PushError(text string) *StatusMessage { return aerc.PushError(text) }
func PushWarning(text string) *StatusMessage { return aerc.PushWarning(text) }
func PushSuccess(text string) *StatusMessage { return aerc.PushSuccess(text) }
func PushStatus(text string, expiry time.Duration) *StatusMessage {
return aerc.PushStatus(text, expiry)
}
func RegisterChoices(choices []Choice) { aerc.RegisterChoices(choices) }
func RegisterPrompt(prompt string, cmd string) { aerc.RegisterPrompt(prompt, cmd) }
func CryptoProvider() crypto.Provider { return aerc.Crypto }
func DecryptKeys(keys []openpgp.Key, symmetric bool) (b []byte, err error) {
return aerc.DecryptKeys(keys, symmetric)
}
func SetKeyPassthrough(value bool) {
config.Viewer().KeyPassthrough = value
for _, name := range AccountNames() {
if acct, _ := Account(name); acct != nil {
acct.SetStatus(state.Passthrough(value))
}
}
}