diff --git a/ui_tree.go b/ui_tree.go index 28c5adc..fe7439d 100644 --- a/ui_tree.go +++ b/ui_tree.go @@ -89,15 +89,12 @@ func (b *Barnard) TreeItemBuild(item uiterm.TreeItem) []uiterm.TreeItem { var treeItem TreeItem if ti, ok := item.(TreeItem); !ok { - root := b.Client.Channels[0] + var root *gumble.Channel + b.Client.Do(func() { root = b.Client.Channels[0] }) if root == nil { return nil } - return []uiterm.TreeItem{ - TreeItem{ - Channel: root, - }, - } + return []uiterm.TreeItem{TreeItem{Channel: root}} } else { treeItem = ti } @@ -108,11 +105,17 @@ func (b *Barnard) TreeItemBuild(item uiterm.TreeItem) []uiterm.TreeItem { users := []uiterm.TreeItem{} ul := []*gumble.User{} - for _, user := range treeItem.Channel.Users { - ul = append(ul, user) - var u = ul[len(ul)-1] - _ = u - } + cl := []*gumble.Channel{} + // TCP handlers mutate both maps; snapshot them while Client.Do holds its + // read lock, then sort/render outside the protocol critical section. + b.Client.Do(func() { + for _, user := range treeItem.Channel.Users { + ul = append(ul, user) + } + for _, subchannel := range treeItem.Channel.Children { + cl = append(cl, subchannel) + } + }) sort.Slice(ul, func(i, j int) bool { return ul[i].Name < ul[j].Name }) @@ -123,10 +126,6 @@ func (b *Barnard) TreeItemBuild(item uiterm.TreeItem) []uiterm.TreeItem { } channels := []uiterm.TreeItem{} - cl := []*gumble.Channel{} - for _, subchannel := range treeItem.Channel.Children { - cl = append(cl, subchannel) - } sort.Slice(cl, func(i, j int) bool { return cl[i].Name < cl[j].Name }) diff --git a/ui_tree_test.go b/ui_tree_test.go new file mode 100644 index 0000000..69db060 --- /dev/null +++ b/ui_tree_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "testing" + + "git.stormux.org/storm/barnard/gumble/gumble" +) + +// Regression: rebuilding the channel tree ranged protocol-owned maps without +// Client.Do while TCP handlers could add or remove users/channels. +func TestTreeItemBuildReadsMapsUnderClientSnapshot(t *testing.T) { + root := &gumble.Channel{ID: 0, Users: gumble.Users{}, Children: gumble.Channels{}} + user := &gumble.User{Session: 1, Name: "user"} + child := &gumble.Channel{ID: 2, Name: "child", Users: gumble.Users{}, Children: gumble.Channels{}} + root.Users[user.Session] = user + root.Children[child.ID] = child + b := &Barnard{Client: &gumble.Client{Channels: gumble.Channels{0: root}}, MutedChannels: map[uint32]bool{}} + items := b.TreeItemBuild(TreeItem{Channel: root}) + if len(items) != 2 { + t.Fatalf("got %d items", len(items)) + } +}