aerc/widgets/account.go
Simon Ser 5685a17674 lib/ui: introduce Invalidatable
Many Drawable implementations have their own Invalidate and OnInvalidate
functions, with an unexported onInvalidate field. However OnInvalidate and
Invalidate are usually not called in the same goroutine. This results in a race
on this field, e.g.:

    Read at 0x00c000094748 by goroutine 7:
      git.sr.ht/~sircmpwn/aerc2/widgets.NewDirectoryList.func1()
          /home/simon/src/aerc2/widgets/dirlist.go:85 +0x56
      git.sr.ht/~sircmpwn/aerc2/widgets.(*Spinner).Start.func1()
          /home/simon/src/aerc2/widgets/spinner.go:93 +0x1bb

    Previous write at 0x00c000094748 by main goroutine:
      [failed to restore the stack]

    Goroutine 7 (running) created at:
      git.sr.ht/~sircmpwn/aerc2/widgets.(*Spinner).Start()
          /home/simon/src/aerc2/widgets/spinner.go:46 +0x8f
      git.sr.ht/~sircmpwn/aerc2/widgets.NewDirectoryList()
          /home/simon/src/aerc2/widgets/dirlist.go:37 +0x286
      git.sr.ht/~sircmpwn/aerc2/widgets.NewAccountView()
          /home/simon/src/aerc2/widgets/account.go:50 +0x5ca
      git.sr.ht/~sircmpwn/aerc2/widgets.NewAerc()
          /home/simon/src/aerc2/widgets/aerc.go:60 +0x800
      main.main()
          /home/simon/src/aerc2/aerc.go:65 +0x33e

To fix this, introduce a new type, Invalidatable, which protects the field.
Unfortunately the Drawable must be passed to the callback function in
Invalidate, so we still need to re-implement this in each Invalidatable user.
2019-04-27 14:30:28 -04:00

190 lines
4.5 KiB
Go

package widgets
import (
"fmt"
"log"
"github.com/gdamore/tcell"
"git.sr.ht/~sircmpwn/aerc2/config"
"git.sr.ht/~sircmpwn/aerc2/lib"
"git.sr.ht/~sircmpwn/aerc2/lib/ui"
"git.sr.ht/~sircmpwn/aerc2/worker"
"git.sr.ht/~sircmpwn/aerc2/worker/types"
)
type AccountView struct {
acct *config.AccountConfig
conf *config.AercConfig
dirlist *DirectoryList
grid *ui.Grid
host TabHost
logger *log.Logger
msglist *MessageList
msgStores map[string]*lib.MessageStore
worker *types.Worker
}
func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
logger *log.Logger, host TabHost) *AccountView {
grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
}).Columns([]ui.GridSpec{
{ui.SIZE_EXACT, conf.Ui.SidebarWidth},
{ui.SIZE_WEIGHT, 1},
})
worker, err := worker.NewWorker(acct.Source, logger)
if err != nil {
host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err))
return &AccountView{
acct: acct,
grid: grid,
host: host,
logger: logger,
}
}
dirlist := NewDirectoryList(acct, logger, worker)
grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT))
msglist := NewMessageList(logger)
grid.AddChild(msglist).At(0, 1)
view := &AccountView{
acct: acct,
conf: conf,
dirlist: dirlist,
grid: grid,
host: host,
logger: logger,
msglist: msglist,
msgStores: make(map[string]*lib.MessageStore),
worker: worker,
}
go worker.Backend.Run()
go func() {
for {
msg := <-worker.Messages
msg = worker.ProcessMessage(msg)
view.onMessage(msg)
}
}()
worker.PostAction(&types.Configure{Config: acct}, nil)
worker.PostAction(&types.Connect{}, view.connected)
host.SetStatus("Connecting...")
return view
}
func (acct *AccountView) Name() string {
return acct.acct.Name
}
func (acct *AccountView) Children() []ui.Drawable {
return acct.grid.Children()
}
func (acct *AccountView) OnInvalidate(onInvalidate func(d ui.Drawable)) {
acct.grid.OnInvalidate(func(_ ui.Drawable) {
onInvalidate(acct)
})
}
func (acct *AccountView) Invalidate() {
acct.grid.Invalidate()
}
func (acct *AccountView) Draw(ctx *ui.Context) {
acct.grid.Draw(ctx)
}
func (acct *AccountView) Focus(focus bool) {
// TODO: Unfocus children I guess
}
func (acct *AccountView) connected(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
acct.host.SetStatus("Listing mailboxes...")
acct.logger.Println("Listing mailboxes...")
acct.dirlist.UpdateList(func(dirs []string) {
var dir string
for _, _dir := range dirs {
if _dir == acct.acct.Default {
dir = _dir
break
}
}
if dir == "" {
dir = dirs[0]
}
acct.dirlist.Select(dir)
acct.logger.Println("Connected.")
acct.host.SetStatus("Connected.")
})
case *types.CertificateApprovalRequest:
// TODO: Ask the user
acct.worker.PostAction(&types.ApproveCertificate{
Message: types.RespondTo(msg),
Approved: true,
}, acct.connected)
}
}
func (acct *AccountView) Directories() *DirectoryList {
return acct.dirlist
}
func (acct *AccountView) Messages() *MessageList {
return acct.msglist
}
func (acct *AccountView) onMessage(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
switch msg.InResponseTo().(type) {
case *types.OpenDirectory:
if store, ok := acct.msgStores[acct.dirlist.selected]; ok {
// If we've opened this dir before, we can re-render it from
// memory while we wait for the update and the UI feels
// snappier. If not, we'll unset the store and show the spinner
// while we download the UID list.
acct.msglist.SetStore(store)
} else {
acct.msglist.SetStore(nil)
}
acct.worker.PostAction(&types.FetchDirectoryContents{},
func(msg types.WorkerMessage) {
store := acct.msgStores[acct.dirlist.selected]
acct.msglist.SetStore(store)
})
}
case *types.DirectoryInfo:
if store, ok := acct.msgStores[msg.Name]; ok {
store.Update(msg)
} else {
acct.msgStores[msg.Name] = lib.NewMessageStore(acct.worker, msg)
}
case *types.DirectoryContents:
store := acct.msgStores[acct.dirlist.selected]
store.Update(msg)
case *types.FullMessage:
store := acct.msgStores[acct.dirlist.selected]
store.Update(msg)
case *types.MessageInfo:
store := acct.msgStores[acct.dirlist.selected]
store.Update(msg)
case *types.MessagesDeleted:
store := acct.msgStores[acct.dirlist.selected]
store.Update(msg)
case *types.Error:
acct.logger.Printf("%v", msg.Error)
acct.host.SetStatus(fmt.Sprintf("%v", msg.Error)).
Color(tcell.ColorDefault, tcell.ColorRed)
}
}