package debugger
import (
"context"
"maps"
"math"
"slices"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/pancsta/cview"
amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
arpc "github.com/pancsta/asyncmachine-go/pkg/rpc"
ssam "github.com/pancsta/asyncmachine-go/pkg/states"
"github.com/pancsta/asyncmachine-go/pkg/telemetry/dbg"
"github.com/pancsta/asyncmachine-go/tools/debugger/states"
"github.com/pancsta/asyncmachine-go/tools/debugger/types"
amrelay "github.com/pancsta/asyncmachine-go/tools/relay"
)
func (d *Debugger ) buildClientList (selectedIndex int ) {
if d .Mach .Not1 (ss .ClientListVisible ) {
return
}
if !d .buildCLScheduled .CompareAndSwap (false , true ) {
return
}
go func () {
time .Sleep (sidebarUpdateDebounce )
update := func () {
d .hBuildClientList (selectedIndex )
d .draw (d .clientList )
}
go d .Mach .Eval ("hBuildClientList" , update , nil )
}()
}
func (d *Debugger ) hBuildClientList (selectedIndex int ) {
defer d .buildCLScheduled .Store (false )
if d .Mach .Not1 (ss .ClientListVisible ) {
return
}
selected := ""
var item *cview .ListItem
if selectedIndex == -1 {
item = d .clientList .GetCurrentItem ()
} else if selectedIndex > -1 {
item = d .clientList .GetItem (selectedIndex )
}
if item != nil {
selected = item .GetReference ().(*sidebarRef ).name
}
d .clientList .Clear ()
var list []string
for _ , c := range d .Clients {
list = append (list , c .Id )
}
humanSort (list )
pos := 0
for _ , parent := range list {
if d .hClientHasParent (parent ) {
continue
}
c := d .Clients [parent ]
if !c .Connected .Load () && d .Mach .Is1 (ss .FilterDisconn ) {
continue
}
if d .Mach .Is1 (ss .FilterRpcMachs ) && machIsRpc (c .MsgStruct ) {
continue
}
item := cview .NewListItem (parent )
item .SetReference (&sidebarRef {name : parent , lvl : 0 })
d .clientList .AddItem (item )
if selected == "" && d .C != nil && d .C .Id == parent {
d .clientList .SetCurrentItem (pos )
} else if selected == parent {
d .clientList .SetCurrentItem (pos )
}
pos = d .hClientListChild (list , parent , pos , selected , 1 )
pos ++
}
var totalSum uint64
for _ , c := range d .Clients {
totalSum += c .MTimeSum
}
d .clientList .SetTitle (d .P .Sprintf (
" Machines:%d T:%v " , len (d .Clients ), totalSum ,
))
d .hUpdateClientList ()
}
func (d *Debugger ) updateClientList () {
if d .buildCLScheduled .Load () || d .Mach .Not1 (ss .ClientListVisible ) {
return
}
if !d .updateCLScheduled .CompareAndSwap (false , true ) {
return
}
go func () {
time .Sleep (sidebarUpdateDebounce )
if !d .updateCLScheduled .CompareAndSwap (true , false ) {
return
}
d .Mach .Eval ("updateClientList" , func () {
d .hUpdateClientList ()
d .drawClientList ()
}, nil )
}()
}
func (d *Debugger ) drawClientList () {
if d .LayoutRoot == nil || d .Mach .Not1 (ss .ClientListVisible ) {
return
}
if n , _ := d .LayoutRoot .GetFrontPanel (); n == "main" {
d .draw (d .clientList )
}
}
func (d *Debugger ) hUpdateClientList () {
defer d .buildCLScheduled .Store (false )
if d .Mach .Not1 (ss .ClientListVisible ) {
return
}
if d .Mach .IsDisposed () || d .Mach .Is1 (ss .SelectingClient ) ||
d .Mach .Is1 (ss .HelpDialog ) {
return
}
_ , _ , width , _ := d .clientList .GetRect ()
maxLen := width - 13
if maxLen < 5 {
maxLen = 15
}
longestName := 0
for _ , item := range d .clientList .GetItems () {
ref := item .GetReference ().(*sidebarRef )
l := len (ref .name ) + ref .lvl
if l > longestName {
longestName = l
}
}
if longestName > maxLen {
longestName = maxLen
}
txtFile := ""
for i , item := range d .clientList .GetItems () {
ref := item .GetReference ().(*sidebarRef )
c := d .Clients [ref .name ]
if c == nil {
continue
}
spacePre := ""
spacePost := " "
hasParent := d .hClientHasParent (c .Id )
if hasParent {
spacePre = " "
}
name := strings .Repeat ("-" , ref .lvl ) + spacePre + ref .name
if len (name ) > maxLen {
name = name [:maxLen -2 ] + ".."
}
spaceCount := int (math .Max (0 , float64 (longestName +1 -len (name ))))
namePad := name +
strings .Repeat (" " , spaceCount ) + spacePost
label := d .hGetClientListLabel (namePad , c , i )
item .SetMainText (label )
if d .params .OutputClients {
if !hasParent {
txtFile += "\n"
}
txtFile += string (cview .StripTags ([]byte (label ), true , false )) + "\n"
for _ , tag := range c .MsgStruct .Tags {
txtFile += strings .Repeat (" " , ref .lvl ) + spacePre +
" #" + tag + "\n"
}
}
}
if len (d .Clients ) > 0 {
var totalSum uint64
for _ , c := range d .Clients {
totalSum += c .MTimeSum
}
d .clientList .SetTitle (d .P .Sprintf (
" Machines:%d T:%v " , len (d .Clients ), totalSum ,
))
} else {
d .clientList .SetTitle (" Machines " )
}
if d .params .OutputClients {
_, _ = d .clientListFile .Seek (0 , 0 )
_ = d .clientListFile .Truncate (0 )
_, _ = d .clientListFile .Write ([]byte (txtFile ))
}
}
type sidebarRef struct {
name string
lvl int
}
func machIsRpc(schema *dbg .DbgMsgStruct ) bool {
return schema .HasTag (arpc .TagRpcClient , "" ) ||
schema .HasTag (arpc .TagRpcServer , "" ) ||
schema .HasTag (arpc .TagRpcMux , "" ) ||
schema .HasTag (amrelay .TagRelay , "" )
}
func (d *Debugger ) hClientListChild (
list []string , parent string , pos int , selected string ,
lvl int ,
) int {
for _ , child := range list {
c := d .Clients [child ]
if !d .hClientHasParent (child ) || c .MsgStruct .Parent != parent {
continue
}
if !c .Connected .Load () && d .Mach .Is1 (ss .FilterDisconn ) {
continue
}
if d .Mach .Is1 (ss .FilterRpcMachs ) && machIsRpc (c .MsgStruct ) {
continue
}
item := cview .NewListItem (child )
item .SetReference (&sidebarRef {name : child , lvl : lvl })
d .clientList .AddItem (item )
if selected == "" && d .C != nil && d .C .Id == child {
d .clientList .SetCurrentItem (pos )
} else if selected == child {
d .clientList .SetCurrentItem (pos )
}
pos = 1 + d .hClientListChild (list , child , pos , selected , lvl +1 )
}
return pos
}
func (d *Debugger ) hClientHasParent (cid string ) bool {
c , ok := d .Clients [cid ]
if !ok || c .MsgStruct .Parent == "" {
return false
}
_, ok = d .Clients [c .MsgStruct .Parent ]
return ok
}
func (d *Debugger ) hGetClientListLabel (
name string , c *Client , index int ,
) string {
isHovered := d .clientList .GetCurrentItemIndex () == index
hasFocus := d .Mach .Is1 (ss .ClientListFocused ) &&
!d .Mach .WillBeAny (states .DebuggerGroups .Focused )
var currCTxIdx int
var currCTx *dbg .DbgMsgTx
currSelTx := d .hCurrentTx ()
if currSelTx != nil {
currTime := d .lastScrolledTxTime
if currTime .IsZero () {
currTime = *currSelTx .Time
}
currCTxIdx = c .TxAtHTime (currTime )
if currCTxIdx != -1 {
currCTx = c .MsgTxs [currCTxIdx ]
}
}
var state string
isErrNow := false
if currCTx != nil {
index := c .MsgStruct .StatesIndex
readyIdx := slices .Index (index , ssam .BasicStates .Ready )
startIdx := slices .Index (index , ssam .BasicStates .Start )
errIdx := slices .Index (index , am .StateException )
isErrNow = errIdx != -1 && am .IsActiveTick (currCTx .Clocks [errIdx ])
if readyIdx != -1 && am .IsActiveTick (currCTx .Clocks [readyIdx ]) {
state = "R"
} else if startIdx != -1 && am .IsActiveTick (currCTx .Clocks [startIdx ]) {
state = "S"
}
if isErrNow {
state = "E" + state
}
}
if state == "" {
state = " "
}
label := d .P .Sprintf ("%s %s|%d" , name , state , currCTxIdx +1 )
if currCTxIdx +1 < len (c .MsgTxs ) {
label += "+"
}
if d .C != nil && c .Id == d .C .Id {
label = "[::bu]" + label
}
if !c .Connected .Load () {
if isHovered && !hasFocus {
label = "[" + theme .Grey + "]" + label
} else if !isHovered {
label = "[" + theme .Grey + "]" + label
} else {
label = "[" + theme .BgPrimary + "]" + label
}
} else if isErrNow && c .Connected .Load () {
label = "[" + theme .Err + "]" + label
} else if c .HadErrSinceTx (currCTxIdx , 100 ) {
label = "[" + theme .ErrRecent + "]" + label
}
return label
}
func (d *Debugger ) hInitClientList () {
d .clientList = cview .NewList ()
d .clientList .SetTitle (" Machines " )
d .clientList .SetBorder (true )
d .clientList .ShowSecondaryText (false )
d .clientList .SetSelectedFocusOnly (true )
d .clientList .SetMainTextColor (tcell .GetColor (theme .Active ))
d .clientList .SetSelectedTextColor (tcell .GetColor (theme .White ))
d .clientList .SetSelectedBackgroundColor (tcell .GetColor (theme .Highlight2 ))
d .clientList .SetHighlightFullLine (true )
d .clientList .SetChangedFunc (func (index int , item *cview .ListItem ) {
go d .Mach .Eval ("clientList.SetChangedFunc" , d .hUpdateClientList , nil )
})
d .clientList .SetSelectedFunc (func (i int , listItem *cview .ListItem ) {
client := listItem .GetReference ().(*sidebarRef )
clickedId := client .name
selectedId := ""
if c , _ := d .Client (); c != nil {
selectedId = c .Id
}
if clickedId == selectedId {
return
}
ctx , cancel := context .WithTimeout (context .Background (), 2 *time .Second )
defer cancel ()
amhelp .Add1Async (
ctx , d .Mach , ss .ClientSelected ,
ss .SelectingClient , Pass (&A {
ClientId : clickedId ,
}),
)
if ctx .Err () != nil {
d .Mach .Log ("timeout when selecting client %s" , clickedId )
return
}
d .Mach .Eval ("clientList.SetSelectedFunc2" , func () {
d .hPrependHistory (&types .MachAddress {MachId : clickedId })
d .hUpdateAddressBar ()
d .draw (d .addressBar )
}, ctx )
})
d .clientList .SetSelectedAlwaysVisible (true )
d .clientList .SetScrollBarColor (tcell .GetColor (theme .Highlight2 ))
}
func (d *Debugger ) hSwitchClient (e *am .Event , diff int ) am .Result {
ids := slices .Collect (maps .Keys (d .Clients ))
var i int
if d .C != nil {
i = slices .Index (ids , d .C .Id )
}
ii := (i + diff ) % len (ids )
if ii < 0 {
ii = len (ids ) - 1
}
return d .Mach .EvAdd1 (e , ss .SelectingClient , Pass (&A {
ClientId : d .Clients [ids [ii ]].Id ,
}))
}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .