mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-10-16 07:34:54 +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>
217 lines
5 KiB
Go
217 lines
5 KiB
Go
package compose
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.sr.ht/~rjarry/aerc/app"
|
|
"git.sr.ht/~rjarry/aerc/commands"
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type Attach struct {
|
|
Menu bool `opt:"-m" desc:"Select files from file-picker-cmd."`
|
|
Name string `opt:"-r" desc:"<name> <cmd...>: Generate attachment from command output."`
|
|
Path string `opt:"path" required:"false" complete:"CompletePath" desc:"Attachment file path."`
|
|
Args string `opt:"..." required:"false"`
|
|
}
|
|
|
|
func init() {
|
|
commands.Register(Attach{})
|
|
}
|
|
|
|
func (Attach) Description() string {
|
|
return "Attach the file at the given path to the email."
|
|
}
|
|
|
|
func (Attach) Context() commands.CommandContext {
|
|
return commands.COMPOSE_EDIT | commands.COMPOSE_REVIEW
|
|
}
|
|
|
|
func (Attach) Aliases() []string {
|
|
return []string{"attach"}
|
|
}
|
|
|
|
func (*Attach) CompletePath(arg string) []string {
|
|
return commands.CompletePath(arg, false)
|
|
}
|
|
|
|
func (a Attach) Execute(args []string) error {
|
|
if a.Menu && a.Name != "" {
|
|
return errors.New("-m and -r are mutually exclusive")
|
|
}
|
|
switch {
|
|
case a.Menu:
|
|
return a.openMenu()
|
|
case a.Name != "":
|
|
if a.Path == "" {
|
|
return errors.New("command is required")
|
|
}
|
|
return a.readCommand()
|
|
default:
|
|
if a.Args != "" {
|
|
return errors.New("only a single path is supported")
|
|
}
|
|
return a.addPath(a.Path)
|
|
}
|
|
}
|
|
|
|
func (a Attach) addPath(path string) error {
|
|
path = xdg.ExpandHome(path)
|
|
attachments, err := filepath.Glob(path)
|
|
if err != nil && errors.Is(err, filepath.ErrBadPattern) {
|
|
log.Warnf("failed to parse as globbing pattern: %v", err)
|
|
attachments = []string{path}
|
|
}
|
|
|
|
if !strings.HasPrefix(path, ".") && !strings.Contains(path, "/.") {
|
|
log.Debugf("removing hidden files from glob results")
|
|
for i := len(attachments) - 1; i >= 0; i-- {
|
|
if strings.HasPrefix(filepath.Base(attachments[i]), ".") {
|
|
if i == len(attachments)-1 {
|
|
attachments = attachments[:i]
|
|
continue
|
|
}
|
|
attachments = append(attachments[:i], attachments[i+1:]...)
|
|
}
|
|
}
|
|
}
|
|
|
|
composer, _ := app.SelectedTabContent().(*app.Composer)
|
|
for _, attach := range attachments {
|
|
log.Debugf("attaching '%s'", attach)
|
|
|
|
pathinfo, err := os.Stat(attach)
|
|
if err != nil {
|
|
log.Errorf("failed to stat file: %v", err)
|
|
app.PushError(err.Error())
|
|
return err
|
|
} else if pathinfo.IsDir() && len(attachments) == 1 {
|
|
app.PushError("Attachment must be a file, not a directory")
|
|
return nil
|
|
}
|
|
|
|
composer.AddAttachment(attach)
|
|
}
|
|
|
|
if len(attachments) == 1 {
|
|
app.PushSuccess(fmt.Sprintf("Attached %s", path))
|
|
} else {
|
|
app.PushSuccess(fmt.Sprintf("Attached %d files", len(attachments)))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a Attach) openMenu() error {
|
|
filePickerCmd := config.Compose().FilePickerCmd
|
|
if filePickerCmd == "" {
|
|
return fmt.Errorf("no file-picker-cmd defined")
|
|
}
|
|
|
|
if strings.Contains(filePickerCmd, "%s") {
|
|
filePickerCmd = strings.ReplaceAll(filePickerCmd, "%s", a.Path)
|
|
}
|
|
|
|
picks, err := os.CreateTemp("", "aerc-filepicker-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var filepicker *exec.Cmd
|
|
if strings.Contains(filePickerCmd, "%f") {
|
|
filePickerCmd = strings.ReplaceAll(filePickerCmd, "%f", picks.Name())
|
|
filepicker = exec.Command("sh", "-c", filePickerCmd)
|
|
} else {
|
|
filepicker = exec.Command("sh", "-c", filePickerCmd+" >&3")
|
|
filepicker.ExtraFiles = append(filepicker.ExtraFiles, picks)
|
|
}
|
|
|
|
t, err := app.NewTerminal(filepicker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.Focus(true)
|
|
t.OnClose = func(err error) {
|
|
defer func() {
|
|
if err := picks.Close(); err != nil {
|
|
log.Errorf("error closing file: %v", err)
|
|
}
|
|
if err := os.Remove(picks.Name()); err != nil {
|
|
log.Errorf("could not remove tmp file: %v", err)
|
|
}
|
|
}()
|
|
|
|
app.CloseDialog()
|
|
|
|
if err != nil {
|
|
log.Errorf("terminal closed with error: %v", err)
|
|
return
|
|
}
|
|
|
|
_, err = picks.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
log.Errorf("seek failed: %v", err)
|
|
return
|
|
}
|
|
|
|
scanner := bufio.NewScanner(picks)
|
|
for scanner.Scan() {
|
|
f := strings.TrimSpace(scanner.Text())
|
|
if _, err := os.Stat(f); err != nil {
|
|
continue
|
|
}
|
|
log.Tracef("File picker attaches: %v", f)
|
|
err := a.addPath(f)
|
|
if err != nil {
|
|
log.Errorf("attach failed for file %s: %v", f, err)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
app.AddDialog(app.DefaultDialog(
|
|
ui.NewBox(t, "File Picker", "", app.SelectedAccountUiConfig()),
|
|
))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a Attach) readCommand() error {
|
|
cmd := exec.Command("sh", "-c", a.Path+" "+a.Args)
|
|
|
|
data, err := cmd.Output()
|
|
if err != nil {
|
|
return errors.Wrap(err, "Output")
|
|
}
|
|
|
|
reader := bufio.NewReader(bytes.NewReader(data))
|
|
|
|
mimeType, mimeParams, err := lib.FindMimeType(a.Name, reader)
|
|
if err != nil {
|
|
return errors.Wrap(err, "FindMimeType")
|
|
}
|
|
|
|
mimeParams["name"] = a.Name
|
|
|
|
composer, _ := app.SelectedTabContent().(*app.Composer)
|
|
err = composer.AddPartAttachment(a.Name, mimeType, mimeParams, reader)
|
|
if err != nil {
|
|
return errors.Wrap(err, "AddPartAttachment")
|
|
}
|
|
|
|
app.PushSuccess(fmt.Sprintf("Attached %s", a.Name))
|
|
|
|
return nil
|
|
}
|