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 {
Invalidatable
text string text string
strategy uint strategy uint
fg tcell.Color fg tcell.Color
bg tcell.Color bg tcell.Color
bold bool bold bool
reverse bool reverse bool
onInvalidate func(d Drawable)
} }
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

@ -20,7 +20,6 @@ type AccountView struct {
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

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,10 +12,10 @@ import (
) )
type MessageList struct { type MessageList struct {
ui.Invalidatable
conf *config.AercConfig conf *config.AercConfig
logger *log.Logger logger *log.Logger
height int height int
onInvalidate func(d ui.Drawable)
scroll int scroll int
selected int selected int
spinner *Spinner spinner *Spinner
@ -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,6 +88,7 @@ func init() {
} }
type Terminal struct { type Terminal struct {
ui.Invalidatable
closed bool closed bool
cmd *exec.Cmd cmd *exec.Cmd
colors map[tcell.Color]tcell.Color colors map[tcell.Color]tcell.Color
@ -98,7 +99,6 @@ type Terminal struct {
destroyed bool destroyed bool
err error err error
focus bool focus bool
onInvalidate func(d ui.Drawable)
pty *os.File pty *os.File
start chan interface{} start chan interface{}
vterm *vterm.VTerm vterm *vterm.VTerm
@ -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) {