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.
This commit is contained in:
Simon Ser 2019-04-27 16:47:59 +00:00 committed by Drew DeVault
parent 9ef2a57b51
commit 5685a17674
12 changed files with 82 additions and 127 deletions

View File

@ -12,6 +12,7 @@ const (
) )
type Bordered struct { type Bordered struct {
Invalidatable
borders uint borders uint
content Drawable content Drawable
onInvalidate func(d Drawable) onInvalidate func(d Drawable)
@ -35,13 +36,7 @@ func (bordered *Bordered) Children() []Drawable {
} }
func (bordered *Bordered) Invalidate() { func (bordered *Bordered) Invalidate() {
if bordered.onInvalidate != nil { bordered.DoInvalidate(bordered)
bordered.onInvalidate(bordered)
}
}
func (bordered *Bordered) OnInvalidate(onInvalidate func(d Drawable)) {
bordered.onInvalidate = onInvalidate
} }
func (bordered *Bordered) Draw(ctx *Context) { func (bordered *Bordered) Draw(ctx *Context) {

View File

@ -6,12 +6,12 @@ import (
) )
type Grid struct { type Grid struct {
Invalidatable
rows []GridSpec rows []GridSpec
rowLayout []gridLayout rowLayout []gridLayout
columns []GridSpec columns []GridSpec
columnLayout []gridLayout columnLayout []gridLayout
cells []*GridCell cells []*GridCell
onInvalidate func(d Drawable)
invalid bool invalid bool
} }
@ -141,9 +141,7 @@ func (grid *Grid) reflow(ctx *Context) {
func (grid *Grid) invalidateLayout() { func (grid *Grid) invalidateLayout() {
grid.invalid = true grid.invalid = true
if grid.onInvalidate != nil { grid.DoInvalidate(grid)
grid.onInvalidate(grid)
}
} }
func (grid *Grid) Invalidate() { func (grid *Grid) Invalidate() {
@ -153,10 +151,6 @@ func (grid *Grid) Invalidate() {
} }
} }
func (grid *Grid) OnInvalidate(onInvalidate func(d Drawable)) {
grid.onInvalidate = onInvalidate
}
func (grid *Grid) AddChild(content Drawable) *GridCell { func (grid *Grid) AddChild(content Drawable) *GridCell {
cell := &GridCell{ cell := &GridCell{
RowSpan: 1, RowSpan: 1,
@ -193,7 +187,5 @@ func (grid *Grid) cellInvalidated(drawable Drawable) {
panic(fmt.Errorf("Attempted to invalidate unknown cell")) panic(fmt.Errorf("Attempted to invalidate unknown cell"))
} }
cell.invalid = true cell.invalid = true
if grid.onInvalidate != nil { grid.DoInvalidate(grid)
grid.onInvalidate(grid)
}
} }

24
lib/ui/invalidatable.go Normal file
View File

@ -0,0 +1,24 @@
package ui
import (
"sync/atomic"
)
type Invalidatable struct {
onInvalidate atomic.Value
}
func (i *Invalidatable) OnInvalidate(f func(d Drawable)) {
i.onInvalidate.Store(f)
}
func (i *Invalidatable) DoInvalidate(d Drawable) {
v := i.onInvalidate.Load()
if v == nil {
return
}
f := v.(func(d Drawable))
if f != nil {
f(d)
}
}

View File

@ -12,13 +12,13 @@ const (
) )
type Text struct { type Text struct {
text string Invalidatable
strategy uint text string
fg tcell.Color strategy uint
bg tcell.Color fg tcell.Color
bold bool bg tcell.Color
reverse bool bold bool
onInvalidate func(d Drawable) reverse bool
} }
func NewText(text string) *Text { func NewText(text string) *Text {
@ -80,12 +80,6 @@ func (t *Text) Draw(ctx *Context) {
ctx.Printf(x, 0, style, t.text) ctx.Printf(x, 0, style, t.text)
} }
func (t *Text) OnInvalidate(onInvalidate func(d Drawable)) {
t.onInvalidate = onInvalidate
}
func (t *Text) Invalidate() { func (t *Text) Invalidate() {
if t.onInvalidate != nil { t.DoInvalidate(t)
t.onInvalidate(t)
}
} }

View File

@ -14,16 +14,15 @@ import (
) )
type AccountView struct { type AccountView struct {
acct *config.AccountConfig acct *config.AccountConfig
conf *config.AercConfig conf *config.AercConfig
dirlist *DirectoryList dirlist *DirectoryList
grid *ui.Grid grid *ui.Grid
host TabHost host TabHost
logger *log.Logger logger *log.Logger
onInvalidate func(d ui.Drawable) msglist *MessageList
msglist *MessageList msgStores map[string]*lib.MessageStore
msgStores map[string]*lib.MessageStore worker *types.Worker
worker *types.Worker
} }
func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig, func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,

View File

@ -12,10 +12,10 @@ import (
) )
type DirectoryList struct { type DirectoryList struct {
ui.Invalidatable
conf *config.AccountConfig conf *config.AccountConfig
dirs []string dirs []string
logger *log.Logger logger *log.Logger
onInvalidate func(d ui.Drawable)
selecting string selecting string
selected string selected string
spinner *Spinner spinner *Spinner
@ -77,14 +77,8 @@ func (dirlist *DirectoryList) Selected() string {
return dirlist.selected return dirlist.selected
} }
func (dirlist *DirectoryList) OnInvalidate(onInvalidate func(d ui.Drawable)) {
dirlist.onInvalidate = onInvalidate
}
func (dirlist *DirectoryList) Invalidate() { func (dirlist *DirectoryList) Invalidate() {
if dirlist.onInvalidate != nil { dirlist.DoInvalidate(dirlist)
dirlist.onInvalidate(dirlist)
}
} }
func (dirlist *DirectoryList) Draw(ctx *ui.Context) { func (dirlist *DirectoryList) Draw(ctx *ui.Context) {

View File

@ -12,6 +12,7 @@ import (
// TODO: scrolling // TODO: scrolling
type ExLine struct { type ExLine struct {
ui.Invalidatable
command []rune command []rune
commit func(cmd string) commit func(cmd string)
ctx *ui.Context ctx *ui.Context
@ -33,14 +34,8 @@ func NewExLine(commit func(cmd string), cancel func()) *ExLine {
} }
} }
func (ex *ExLine) OnInvalidate(onInvalidate func(d ui.Drawable)) {
ex.onInvalidate = onInvalidate
}
func (ex *ExLine) Invalidate() { func (ex *ExLine) Invalidate() {
if ex.onInvalidate != nil { ex.DoInvalidate(ex)
ex.onInvalidate(ex)
}
} }
func (ex *ExLine) Draw(ctx *ui.Context) { func (ex *ExLine) Draw(ctx *ui.Context) {

View File

@ -12,14 +12,14 @@ import (
) )
type MessageList struct { type MessageList struct {
conf *config.AercConfig ui.Invalidatable
logger *log.Logger conf *config.AercConfig
height int logger *log.Logger
onInvalidate func(d ui.Drawable) height int
scroll int scroll int
selected int selected int
spinner *Spinner spinner *Spinner
store *lib.MessageStore store *lib.MessageStore
} }
// TODO: fish in config // TODO: fish in config
@ -37,14 +37,8 @@ func NewMessageList(logger *log.Logger) *MessageList {
return ml return ml
} }
func (ml *MessageList) OnInvalidate(onInvalidate func(d ui.Drawable)) {
ml.onInvalidate = onInvalidate
}
func (ml *MessageList) Invalidate() { func (ml *MessageList) Invalidate() {
if ml.onInvalidate != nil { ml.DoInvalidate(ml)
ml.onInvalidate(ml)
}
} }
func (ml *MessageList) Draw(ctx *ui.Context) { func (ml *MessageList) Draw(ctx *ui.Context) {

View File

@ -252,8 +252,7 @@ func (mv *MessageViewer) Focus(focus bool) {
} }
type HeaderView struct { type HeaderView struct {
onInvalidate func(d ui.Drawable) ui.Invalidatable
Name string Name string
Value string Value string
} }
@ -281,17 +280,11 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
} }
func (hv *HeaderView) Invalidate() { func (hv *HeaderView) Invalidate() {
if hv.onInvalidate != nil { hv.DoInvalidate(hv)
hv.onInvalidate(hv)
}
}
func (hv *HeaderView) OnInvalidate(fn func(d ui.Drawable)) {
hv.onInvalidate = fn
} }
type MultipartView struct { type MultipartView struct {
onInvalidate func(d ui.Drawable) ui.Invalidatable
} }
func (mpv *MultipartView) Draw(ctx *ui.Context) { func (mpv *MultipartView) Draw(ctx *ui.Context) {
@ -303,11 +296,5 @@ func (mpv *MultipartView) Draw(ctx *ui.Context) {
} }
func (mpv *MultipartView) Invalidate() { func (mpv *MultipartView) Invalidate() {
if mpv.onInvalidate != nil { mpv.DoInvalidate(mpv)
mpv.onInvalidate(mpv)
}
}
func (mpv *MultipartView) OnInvalidate(fn func(d ui.Drawable)) {
mpv.onInvalidate = fn
} }

View File

@ -23,8 +23,8 @@ var (
) )
type Spinner struct { type Spinner struct {
ui.Invalidatable
frame int64 // access via atomic frame int64 // access via atomic
onInvalidate func(d ui.Drawable)
stop chan struct{} stop chan struct{}
} }
@ -84,12 +84,6 @@ func (s *Spinner) Draw(ctx *ui.Context) {
ctx.Printf(col, 0, tcell.StyleDefault, "%s", frames[cur]) ctx.Printf(col, 0, tcell.StyleDefault, "%s", frames[cur])
} }
func (s *Spinner) OnInvalidate(onInvalidate func(d ui.Drawable)) {
s.onInvalidate = onInvalidate
}
func (s *Spinner) Invalidate() { func (s *Spinner) Invalidate() {
if s.onInvalidate != nil { s.DoInvalidate(s)
s.onInvalidate(s)
}
} }

View File

@ -9,10 +9,9 @@ import (
) )
type StatusLine struct { type StatusLine struct {
ui.Invalidatable
stack []*StatusMessage stack []*StatusMessage
fallback StatusMessage fallback StatusMessage
onInvalidate func(d ui.Drawable)
} }
type StatusMessage struct { type StatusMessage struct {
@ -31,14 +30,8 @@ func NewStatusLine() *StatusLine {
} }
} }
func (status *StatusLine) OnInvalidate(onInvalidate func(d ui.Drawable)) {
status.onInvalidate = onInvalidate
}
func (status *StatusLine) Invalidate() { func (status *StatusLine) Invalidate() {
if status.onInvalidate != nil { status.DoInvalidate(status)
status.onInvalidate(status)
}
} }
func (status *StatusLine) Draw(ctx *ui.Context) { func (status *StatusLine) Draw(ctx *ui.Context) {

View File

@ -88,20 +88,20 @@ func init() {
} }
type Terminal struct { type Terminal struct {
closed bool ui.Invalidatable
cmd *exec.Cmd closed bool
colors map[tcell.Color]tcell.Color cmd *exec.Cmd
ctx *ui.Context colors map[tcell.Color]tcell.Color
cursorPos vterm.Pos ctx *ui.Context
cursorShown bool cursorPos vterm.Pos
damage []vterm.Rect cursorShown bool
destroyed bool damage []vterm.Rect
err error destroyed bool
focus bool err error
onInvalidate func(d ui.Drawable) focus bool
pty *os.File pty *os.File
start chan interface{} start chan interface{}
vterm *vterm.VTerm vterm *vterm.VTerm
OnClose func(err error) OnClose func(err error)
OnStart func() OnStart func()
@ -225,10 +225,6 @@ func (term *Terminal) Destroy() {
term.destroyed = true term.destroyed = true
} }
func (term *Terminal) OnInvalidate(cb func(d ui.Drawable)) {
term.onInvalidate = cb
}
func (term *Terminal) Invalidate() { func (term *Terminal) Invalidate() {
if term.vterm != nil { if term.vterm != nil {
width, height := term.vterm.Size() width, height := term.vterm.Size()
@ -239,9 +235,7 @@ func (term *Terminal) Invalidate() {
} }
func (term *Terminal) invalidate() { func (term *Terminal) invalidate() {
if term.onInvalidate != nil { term.DoInvalidate(term)
term.onInvalidate(term)
}
} }
func (term *Terminal) Draw(ctx *ui.Context) { func (term *Terminal) Draw(ctx *ui.Context) {