mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-10-25 03:08:15 +02:00
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>
233 lines
5.1 KiB
Go
233 lines
5.1 KiB
Go
package commands
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"git.sr.ht/~rjarry/aerc/app"
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/go-opt/v2"
|
|
)
|
|
|
|
type Menu struct {
|
|
ErrExit bool `opt:"-e" desc:"Stop executing commands on the first error."`
|
|
Background bool `opt:"-b" desc:"Do NOT spawn the popover dialog."`
|
|
Accounts bool `opt:"-a" desc:"Feed command with account names."`
|
|
Directories bool `opt:"-d" desc:"Feed command with folder names."`
|
|
Command string `opt:"-c" desc:"Override [general].default-menu-cmd."`
|
|
Title string `opt:"-t" desc:"Override the default menu title."`
|
|
Xargs string `opt:"..." complete:"CompleteXargs" desc:"Command name."`
|
|
}
|
|
|
|
func init() {
|
|
Register(Menu{})
|
|
}
|
|
|
|
func (Menu) Description() string {
|
|
return "Open a popover dialog."
|
|
}
|
|
|
|
func (Menu) Context() CommandContext {
|
|
return GLOBAL
|
|
}
|
|
|
|
func (Menu) Aliases() []string {
|
|
return []string{"menu"}
|
|
}
|
|
|
|
func (*Menu) CompleteXargs(arg string) []string {
|
|
return FilterList(ActiveCommandNames(), arg, nil)
|
|
}
|
|
|
|
func (m Menu) Execute([]string) error {
|
|
if m.Command == "" {
|
|
m.Command = config.General().DefaultMenuCmd
|
|
}
|
|
useFallback := m.useFallback()
|
|
if m.Background && useFallback {
|
|
return errors.New("Either -c <command> or " +
|
|
"default-menu-cmd is required to run " +
|
|
"in the background.")
|
|
}
|
|
if _, _, err := ResolveCommand(m.Xargs, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
lines, err := m.feedLines()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
title := m.Title
|
|
if len(title) == 0 {
|
|
title = " :" + strings.TrimLeft(m.Xargs, ": \t") + " ... "
|
|
}
|
|
|
|
if useFallback {
|
|
return m.fallback(title, lines)
|
|
}
|
|
|
|
pick, err := os.CreateTemp("", "aerc-menu-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var proc *exec.Cmd
|
|
if strings.Contains(m.Command, "%f") {
|
|
proc = exec.Command("sh", "-c",
|
|
strings.ReplaceAll(m.Command, "%f", opt.QuoteArg(pick.Name())))
|
|
} else {
|
|
proc = exec.Command("sh", "-c", m.Command+" >&3")
|
|
proc.ExtraFiles = append(proc.ExtraFiles, pick)
|
|
}
|
|
if len(lines) > 0 {
|
|
proc.Stdin = strings.NewReader(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
xargs := func(err error) {
|
|
var buf []byte
|
|
if err == nil {
|
|
_, err = pick.Seek(0, io.SeekStart)
|
|
}
|
|
if err == nil {
|
|
buf, err = io.ReadAll(pick)
|
|
}
|
|
pick.Close()
|
|
os.Remove(pick.Name())
|
|
if err != nil {
|
|
app.PushError("command failed: " + err.Error())
|
|
return
|
|
}
|
|
if len(buf) == 0 {
|
|
return
|
|
}
|
|
m.runCmd(string(buf))
|
|
}
|
|
|
|
if m.Background {
|
|
go func() {
|
|
defer log.PanicHandler()
|
|
xargs(proc.Run())
|
|
}()
|
|
} else {
|
|
term, err := app.NewTerminal(proc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
term.Focus(true)
|
|
term.OnClose = func(err error) {
|
|
app.CloseDialog()
|
|
xargs(err)
|
|
}
|
|
|
|
widget := ui.NewBox(term, title, "", app.SelectedAccountUiConfig())
|
|
app.AddDialog(app.DefaultDialog(widget))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m Menu) useFallback() bool {
|
|
if m.Command == "" || m.Command == "-" {
|
|
return true
|
|
}
|
|
cmd, _, _ := strings.Cut(m.Command, " ")
|
|
_, err := exec.LookPath(cmd)
|
|
if err != nil {
|
|
warnMsg := "command '" + cmd + "' not found in PATH, " +
|
|
"falling back on aerc's picker."
|
|
log.Warnf(warnMsg)
|
|
app.PushWarning(warnMsg)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m Menu) runCmd(buffer string) {
|
|
var (
|
|
cmd Command
|
|
cmdline string
|
|
err error
|
|
)
|
|
|
|
for _, line := range strings.Split(buffer, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
cmdline = m.Xargs + " " + line
|
|
cmdline, cmd, err = ResolveCommand(cmdline, nil, nil)
|
|
if err == nil {
|
|
err = ExecuteCommand(cmd, cmdline)
|
|
}
|
|
if err != nil {
|
|
app.PushError(m.Xargs + ": " + err.Error())
|
|
if m.ErrExit {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m Menu) fallback(title string, lines []string) error {
|
|
listBox := app.NewListBox(
|
|
title, lines, app.SelectedAccountUiConfig(),
|
|
func(line string) {
|
|
app.CloseDialog()
|
|
if line == "" {
|
|
return
|
|
}
|
|
m.runCmd(line)
|
|
})
|
|
listBox.SetTextFilter(func(list []string, term string) []string {
|
|
return FilterList(list, term, func(s string) string { return s })
|
|
})
|
|
widget := ui.NewBox(listBox, "", "", app.SelectedAccountUiConfig())
|
|
app.AddDialog(app.DefaultDialog(widget))
|
|
return nil
|
|
}
|
|
|
|
func (m Menu) feedLines() ([]string, error) {
|
|
var lines []string
|
|
|
|
switch {
|
|
case m.Accounts && m.Directories:
|
|
for _, a := range app.AccountNames() {
|
|
account, _ := app.Account(a)
|
|
a = opt.QuoteArg(a)
|
|
for _, d := range account.Directories().List() {
|
|
dir := account.Directories().Directory(d)
|
|
if dir != nil && dir.Role != models.QueryRole {
|
|
d = opt.QuoteArg(d)
|
|
}
|
|
lines = append(lines, a+" "+d)
|
|
}
|
|
}
|
|
|
|
case m.Accounts:
|
|
for _, account := range app.AccountNames() {
|
|
lines = append(lines, opt.QuoteArg(account))
|
|
}
|
|
|
|
case m.Directories:
|
|
account := app.SelectedAccount()
|
|
if account == nil {
|
|
return nil, errors.New("No account selected.")
|
|
}
|
|
for _, d := range account.Directories().List() {
|
|
dir := account.Directories().Directory(d)
|
|
if dir != nil && dir.Role != models.QueryRole {
|
|
d = opt.QuoteArg(d)
|
|
}
|
|
lines = append(lines, d)
|
|
}
|
|
}
|
|
|
|
return lines, nil
|
|
}
|