package debugger
import (
"bufio"
"context"
_ "embed"
"encoding/gob"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/andybalholm/brotli"
"github.com/charmbracelet/ssh"
"github.com/gdamore/tcell/v2"
"github.com/pancsta/cview"
"github.com/soheilhy/cmux"
"github.com/zyedidia/clipper"
"golang.org/x/text/language"
"golang.org/x/text/message"
"github.com/pancsta/asyncmachine-go/internal/utils"
amgraph "github.com/pancsta/asyncmachine-go/pkg/graph"
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/server"
"github.com/pancsta/asyncmachine-go/tools/debugger/states"
"github.com/pancsta/asyncmachine-go/tools/debugger/types"
)
type (
S = am .S
A = types .A
)
var (
Pass = am .Pass
ss = states .DebuggerStates
P = message .NewPrinter (language .English )
)
var theme Theme
type Debugger struct {
*am .ExceptionHandler
*ssam .DisposedHandlers
Mach *am .Machine
Clients map [string ]*Client
graphHash string
LayoutRoot *cview .Panels
C *Client
App *cview .Application
History []*types .MachAddress
HistoryCursor int
params types .Params
Params atomic .Pointer [types .Params ]
drawing atomic .Bool
tree *cview .TreeView
treeRoot *cview .TreeNode
log *cview .TextView
timelineTxs *cview .ProgressBar
timelineSteps *cview .ProgressBar
focusable []*cview .Box
playTimer *time .Ticker
currTxBarRight *cview .TextView
currTxBarLeft *cview .TextView
nextTxBarLeft *cview .TextView
nextTxBarRight *cview .TextView
helpDialog *cview .Flex
statusBarRight *cview .TextView
clientList *cview .List
mainGrid *cview .Grid
logRebuildEnd int
lastScrolledTxTime time .Time
repaintScheduled atomic .Bool
repaintPending atomic .Bool
graph *amgraph .Graph
updateCLScheduled atomic .Bool
buildCLScheduled atomic .Bool
lastKeystroke tcell .Key
lastKeystrokeTime time .Time
matrix *cview .Table
exportDialog *cview .Modal
contentPanels *cview .Panels
toolbars [4 ]*cview .Table
schemaLogGrid *cview .Grid
treeMatrixGrid *cview .Grid
selectedState atomic .Pointer [string ]
selectedClient atomic .Pointer [string ]
selectedSchemaHash atomic .Pointer [string ]
redrawCallback func ()
heartbeatT *time .Ticker
logReader *cview .TreeView
helpDialogLeft *cview .TextView
helpDialogRight *cview .TextView
addressBar *cview .Table
tagsBar *cview .TextView
clip clipper .Clipboard
toolbarItems [4 ][]toolbarItem
clientListFile *os .File
txFileMd *os .File
msgsDelayed []*dbg .DbgMsgTx
msgsDelayedConns []string
currTxBar *cview .Flex
nextTxBar *cview .Flex
mainGridCols []int
logReaderExpanded map [string ]bool
logReaderScroll int
logReaderSelectedY int
logReaderSelected string
logReaderSelectedLevel int
logReaderSelectedParent string
treeGroups *cview .DropDown
treeLayout *cview .Flex
schemaTreeStates am .S
selectedGroup atomic .Pointer [string ]
logAppends int
logRenderedClient string
lastResize uint64
logLastResize uint64
sshSrv *ssh .Server
logFile *os .File
logFileMx sync .Mutex
statusBarLeft *cview .TextView
focusablePrims []cview .Primitive
mouseFocusChanged bool
Focused cview .Primitive
treeGroupSkip bool
preModalFocus cview .Primitive
overlay *cview .TextView
graphFileMd *os .File
graphFileMgml *os .File
ServerMux cmux .CMux
ServerHttp *http .Server
ctxCancelCursor context .CancelFunc
ctxCursor context .Context
callLogFiles map [string ]*os .File
callLogFilesLen map [string ]int64
callLogCount map [string ]int
callLogLastSep map [string ]int
listenHost string
listenAddrRpc string
listenAddrHttp string
listenAddrSsh string
diagMachDom atomic .Pointer [goquery .Document ]
diagMachName atomic .Pointer [string ]
diagStateDom atomic .Pointer [goquery .Document ]
diagStatePath atomic .Pointer [string ]
diagMachUpdate chan struct {}
diagStepsUpdate chan struct {}
diagStateUpdate chan struct {}
diagSkipGroup atomic .Pointer [string ]
diagStepsFileD2 *os .File
diagStepsFileD2Svg *os .File
diagStepsFileMermaid *os .File
diagStepsFileMermaidAscii *os .File
loadingPos int
}
func New (ctx context .Context , p types .Params ) (*Debugger , error ) {
var err error
d := &Debugger {
DisposedHandlers : &ssam .DisposedHandlers {},
Clients : make (map [string ]*Client ),
logReaderExpanded : make (map [string ]bool ),
callLogFiles : make (map [string ]*os .File ),
callLogFilesLen : make (map [string ]int64 ),
callLogCount : make (map [string ]int ),
callLogLastSep : make (map [string ]int ),
diagMachUpdate : make (chan struct {}, 1 ),
diagStepsUpdate : make (chan struct {}, 1 ),
diagStateUpdate : make (chan struct {}, 1 ),
}
d .diagSkipGroup .Store (new (string ))
d .diagMachName .Store (new (string ))
d .diagStatePath .Store (new (string ))
d .selectedState .Store (new (string ))
d .selectedGroup .Store (new (string ))
d .selectedSchemaHash .Store (new (string ))
d .selectedClient .Store (new (string ))
id := utils .RandId (0 )
if p .Id != "" {
id = p .Id
}
mach , err := am .NewCommon (ctx , "d-" +id , states .DebuggerSchema ,
ss .Names (), d , nil , &am .Opts {
DontLogId : true ,
Tags : []string {"am-dbg" },
})
if err != nil {
return nil , err
}
d .Mach = mach
mach .SetGroups (states .DebuggerGroups , ss )
if p .DebugAddr != "" {
_ = amhelp .MachDebug (mach , p .DebugAddr , p .LogLevel , false ,
amhelp .SemConfigEnv (true ))
}
if p .Repl {
err = arpc .MachRepl (mach , "" , &arpc .ReplOpts {
AddrDir : p .OutputDir ,
Args : types .ArgsRpc ,
})
mach .AddErr (err , nil )
}
err = d .hSetParams (p )
if err != nil {
return nil , err
}
if d .params .Version == "" {
d .params .Version = "(devel)"
}
semLog := mach .SemLogger ()
if d .params .DbgLogger != nil {
semLog .SetSimple (d .params .DbgLogger .Printf , d .params .LogLevel )
} else {
semLog .SetSimple (log .Printf , d .params .LogLevel )
}
semLog .SetArgsMapper (amhelp .LogArgsMapper )
d .graph , err = amgraph .New (d .Mach )
if err != nil {
mach .AddErr (fmt .Errorf ("graph init: %w" , err ), nil )
}
if d .params .ImportData != "" {
d .params .Print ("Importing data from %s\nPlease wait...\n" ,
d .params .ImportData )
start := time .Now ()
mach .Log ("Importing data from %s" , d .params .ImportData )
d .hImportData (d .params .ImportData )
if d .Mach .IsErr () {
d .params .Print ("ERROR: %s\n" , d .Mach .Err ())
} else {
mach .Log ("Imported data in %s" , time .Since (start ))
}
}
mach .OnDispose (func (id string , ctx context .Context ) {
d .Dispose ()
})
return d , nil
}
func (d *Debugger ) hSetParams (p types .Params ) error {
mach := d .Mach
if p .LogLevel > am .LogEverything {
p .LogLevel = am .LogEverything
}
if p .FilterLogLevel > am .LogEverything {
p .FilterLogLevel = am .LogEverything
}
p .OutputDiagrams .Value = max (p .OutputDiagrams .Value , p .OutputDiagrams .Value )
p .ViewTimelines .Value = max (p .ViewTimelines .Value , p .ViewTimelines .Value )
if p .ViewRain && p .StartupView == "tree-log" {
p .StartupView = "tree-matrix"
}
httpAddr := ""
sshAddr := ""
rpcAddr := p .ListenAddr
if p .ListenAddr != "-1" && p .ListenAddr != "" {
host , port , err := net .SplitHostPort (p .ListenAddr )
d .listenHost = host
if host == "0.0.0.0" {
listenHost , err := utils .GetGlobalUnicastIP ()
if err != nil {
return err
}
d .listenHost = listenHost
mach .Log ("public host: %s" , listenHost )
}
if err == nil {
dbgPort , _ := strconv .Atoi (port )
httpAddr = host + ":" + strconv .Itoa (dbgPort +1 )
sshAddr = host + ":" + strconv .Itoa (dbgPort +2 )
d .listenAddrRpc = d .listenHost + ":" + strconv .Itoa (dbgPort )
d .listenAddrHttp = d .listenHost + ":" + strconv .Itoa (dbgPort +1 )
d .listenAddrSsh = d .listenHost + ":" + strconv .Itoa (dbgPort +2 )
}
}
if !p .UiSsh {
sshAddr = ""
}
if !p .UiWeb {
httpAddr = ""
}
p .AddrRpc = rpcAddr
p .AddrHttp = httpAddr
p .AddrSsh = sshAddr
p .UiSsh = p .UiSsh && sshAddr != ""
p .UiWeb = p .UiWeb && httpAddr != ""
p .TailMode = p .TailMode && p .MachUrl == ""
p .Version = utils .GetVersion ()
if p .Filters == nil {
p .Filters = &types .Filters {
LogLevel : p .FilterLogLevel ,
SkipOutGroup : p .FilterGroup ,
SkipCanceledTx : p .FilterCanceledTx ,
SkipAutoTx : p .FilterAutoTx ,
SkipAutoCanceledTx : p .FilterAutoCanceledTx ,
SkipEmptyTx : p .FilterEmptyTx ,
SkipHealthTx : p .FilterHealthTx ,
SkipQueuedTx : p .FilterQueuedTx ,
SkipChecks : p .FilterChecks ,
SkipRpcMach : p .FilterRpcMachs ,
}
}
d .statesFromFilters (p .Filters )
if p .Print == nil {
p .Print = func (txt string , args ...any ) {
fmt .Printf (txt , args ...)
}
}
if p .FilterDisconn {
d .Mach .Add1 (ss .FilterDisconn , nil )
} else {
d .Mach .Remove1 (ss .FilterDisconn , nil )
}
var err error
if p .ViewTheme == "light" {
theme , err = mapToTheme (themeLight , false )
} else {
theme , err = mapToTheme (themeDark , true )
}
if err != nil {
return err
}
theme .Apply ()
if p .EnableClipboard {
clip , err := clipper .GetClipboard (clipper .Clipboards ...)
if err != nil {
mach .AddErr (fmt .Errorf ("clipboard init: %w" , err ), nil )
}
d .clip = clip
}
err = os .MkdirAll (path .Join (p .OutputDir , "diagrams" ), 0o755 )
if err != nil {
return err
}
err = os .MkdirAll (path .Join (p .OutputDir , "call-log" ), 0o755 )
if err != nil {
return err
}
if p .OutputClients {
p := path .Join (p .OutputDir , "clients.txt" )
clientListFile , err := os .Create (p )
if err != nil {
mach .AddErr (err , nil )
}
d .clientListFile = clientListFile
}
if p .OutputGraph {
loc := path .Join (p .OutputDir , "graph.md" )
d .graphFileMd , err = os .Create (loc )
if err != nil {
mach .AddErr (err , nil )
}
loc = path .Join (p .OutputDir , "graph.xml" )
d .graphFileMgml , err = os .Create (loc )
if err != nil {
mach .AddErr (err , nil )
}
}
d .logReaderExpanded ["__link_nodes" ] = p .ViewExpandLinks
d .params = p
clone := p
d .Params .Store (&clone )
if p .OutputLog {
d .hInitLogFile ()
}
if p .OutputTx {
if err := d .hInitTxFile (); err != nil {
return err
}
}
if p .OutputDiagrams != types .ParamsOutputDiagramsNone {
if err := d .hInitDiagFiles (); err != nil {
return err
}
}
return nil
}
func (d *Debugger ) hInitTxFile () error {
loc := path .Join (d .params .OutputDir , "tx.md" )
txFile , err := os .Create (loc )
if err != nil {
return err
}
d .txFileMd = txFile
return nil
}
func (d *Debugger ) hCloseTxFile () error {
if d .txFileMd == nil {
return nil
}
return d .txFileMd .Close ()
}
func (d *Debugger ) hInitLogFile () {
p := path .Join (d .params .OutputDir , logFile )
logFile , err := os .Create (p )
if err != nil {
d .Mach .AddErr (err , nil )
return
}
d .logFile = logFile
}
func (d *Debugger ) hCloseLogFile () {
d .logFileMx .Lock ()
defer d .logFileMx .Unlock ()
d .Mach .AddErr (d .logFile .Close (), nil )
d .logFile = nil
}
func (d *Debugger ) hCloseCallLogFiles () {
for _ , f := range d .callLogFiles {
f .Close ()
}
d .callLogFiles = make (map [string ]*os .File )
}
func (d *Debugger ) hInitDiagFiles () error {
dir := d .params .OutputDir
diagDir := path .Join (dir , "diagrams" )
var err error
txD2File := path .Join (diagDir , "steps.d2" )
d .diagStepsFileD2 , err = os .Create (txD2File )
if err != nil {
return nil
}
txD2SvgFile := path .Join (diagDir , "steps.d2.svg" )
d .diagStepsFileD2Svg , err = os .Create (txD2SvgFile )
if err != nil {
return err
}
txMermaidFile := path .Join (diagDir , "steps.mermaid" )
d .diagStepsFileMermaid , err = os .Create (txMermaidFile )
if err != nil {
return err
}
txMermaidAsciiFile := path .Join (diagDir , "steps.mermaid.txt" )
d .diagStepsFileMermaidAscii , err = os .Create (txMermaidAsciiFile )
if err != nil {
return err
}
return nil
}
func (d *Debugger ) hCloseDiagFiles () {
if d .diagStepsFileD2Svg == nil {
return
}
d .diagStepsFileD2Svg .Close ()
d .diagStepsFileD2 .Close ()
d .diagStepsFileMermaid .Close ()
d .diagStepsFileMermaidAscii .Close ()
}
func (d *Debugger ) hSetCursor1 (e *am .Event , args *A ) {
cursor1 := args .Cursor1
cursorStep1 := args .CursorStep1
skipHistory := args .SkipHistory
trimHistory := args .TrimHistory
filterBack := args .FilterBack
c := d .C
tx := d .hCurrentTx ()
if d .ctxCancelCursor != nil {
d .ctxCancelCursor ()
}
d .ctxCursor , d .ctxCancelCursor = context .WithCancel (d .Mach .Context ())
ctx := am .EvToCtx (d .ctxCursor , e )
c .CursorTx1 = d .hFilterTxCursor1 (c , cursor1 , filterBack )
c .CursorStep1 = cursorStep1
if d .HistoryCursor == 0 && !skipHistory {
d .hPrependHistory (d .hGetMachAddress ())
} else if trimHistory {
d .hTrimHistory ()
}
d .hHandleTStepsScrolled ()
d .lastScrolledTxTime = time .Time {}
if tx != nil {
d .Mach .EvAdd1 (e , ss .TxSelected , nil )
tx := d .hCurrentTx ()
if tx != nil {
d .lastScrolledTxTime = *tx .Time
}
if d .params .OutputDiagrams != types .ParamsOutputDiagramsNone &&
tx != nil {
d .Mach .GoAfter (d .ctxCursor , time .Second /2 , func () {
parsed := c .TxParsed (c .CursorTx1 - 1 )
if parsed == nil {
err := fmt .Errorf ("parsed tx missing for %s" , tx .ID )
d .Mach .AddErrState (ss .ErrDiagrams , err , nil )
return
}
d .hDiagramsStepsRendering (ctx , tx , parsed )
})
}
} else {
d .Mach .EvRemove1 (e , ss .TxSelected , nil )
}
d .Mach .EvRemove1 (e , ss .TimelineStepsScrolled , nil )
d .Mach .GoAfter (ctx , time .Second , func () {
if err := d .diagramsMachUpdating (ctx ); err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
})
d .Mach .GoAfter (ctx , time .Second , func () {
if err := d .diagramsStateUpdating (ctx ); err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
})
}
func (d *Debugger ) hUpdateGraphHash () {
schemas := ""
for _ , c := range d .Clients {
schemas += amhelp .SchemaHash (c .MsgStruct .States )
}
d .graphHash = am .Hash (schemas , 20 )
}
func (d *Debugger ) hGetMachAddress () *types .MachAddress {
c := d .C
if c == nil {
return nil
}
a := &types .MachAddress {
MachId : c .Id ,
}
if c .CursorTx1 > 0 {
tx := c .MsgTxs [c .CursorTx1 -1 ]
a .TxId = tx .ID
a .MachTime = tx .TimeSum ()
}
if c .CursorStep1 > 0 {
a .Step = c .CursorStep1
}
a .State = c .SelectedState
a .Group = c .SelectedGroup
return a
}
func (d *Debugger ) GoToMachAddress (
addr *types .MachAddress , skipHistory bool ,
) bool {
ctx := context .TODO ()
if addr .MachId == "" {
return false
}
var wait <-chan struct {}
mach := d .Mach
if d .C == nil || d .C .Id != addr .MachId {
if mach .Is1 (ss .ClientSelected ) {
wait = mach .WhenTicks (ss .ClientSelected , 2 , ctx )
} else {
wait = mach .When1 (ss .ClientSelected , ctx )
}
res := mach .Add1 (ss .SelectingClient , Pass (&A {
ClientId : addr .MachId ,
}))
if res == am .Canceled {
return false
}
} else {
wait = mach .When1 (ss .ClientSelected , ctx )
}
<-wait
if addr .TxId != "" {
scrollArgs := &A {
TxId : addr .TxId ,
CursorStep1 : addr .Step ,
}
mach .Add1 (ss .ScrollToTx , Pass (scrollArgs ))
} else if addr .MachTime != 0 {
tx := d .C .Tx (d .C .TxAtMachTime (addr .MachTime ))
mach .Add1 (ss .ScrollToTx , Pass (&A {
TxId : tx .ID ,
}))
} else if !addr .HumanTime .IsZero () {
tx := d .C .Tx (d .C .TxAtHTime (addr .HumanTime ))
mach .Add1 (ss .ScrollToTx , Pass (&A {
TxId : tx .ID ,
}))
} else if addr .QueueTick != 0 {
tx := d .C .Tx (d .C .TxAtQueueTick (addr .QueueTick ))
mach .Add1 (ss .ScrollToTx , Pass (&A {
TxId : tx .ID ,
}))
}
d .hUpdateAddressBar ()
if addr .State != "" {
d .Mach .Add1 (ss .StateNameSelected , Pass (&A {
State : addr .State ,
}))
}
if addr .Group != "" {
d .Mach .Eval ("GoToMachAddress" , func () {
label := ""
for l := range d .C .MsgStruct .Groups {
if types .NormalizeGroupName (l ) == types .NormalizeGroupName (addr .Group ) {
label = l
break
}
}
d .selectedGroup .Store (&label )
d .C .SelectedGroup = label
d .hBuildSchemaTree ()
d .hUpdateSchemaTree ()
d .hUpdateTreeGroups ()
d .Mach .Add (am .S {ss .ToolToggled , ss .UpdateLogScheduled }, Pass (&A {
FilterTxs : true ,
LogRebuildEnd : len (d .C .MsgTxs ),
}))
}, nil )
} else {
d .Mach .Add1 (ss .SetGroup , Pass (&A {
Group : "all" ,
}))
}
if addr .Step != 0 {
mach .Add1 (ss .ScrollToStep , Pass (&A {
CursorStep1 : addr .Step ,
}))
}
return true
}
func (d *Debugger ) hRemoveHistory (clientId string ) {
hist := make ([]*types .MachAddress , 0 )
for i , item := range d .History {
if i <= d .HistoryCursor && d .HistoryCursor > 0 {
d .HistoryCursor --
}
if item .MachId == clientId {
continue
}
hist = append (hist , item )
}
d .History = hist
}
func (d *Debugger ) hPrependHistory (addr *types .MachAddress ) {
if len (d .History ) > 0 && d .History [0 ].StringBase () == addr .StringBase () {
return
}
d .History = slices .Concat ([]*types .MachAddress {addr }, d .History )
d .hTrimHistory ()
}
func (d *Debugger ) hTrimHistory () {
if d .HistoryCursor > 0 {
rm := d .HistoryCursor
if rm >= len (d .History ) {
rm = len (d .History )
}
d .History = d .History [rm :]
}
d .HistoryCursor = 0
if len (d .History ) > 100 {
d .History = d .History [100 :]
}
}
func (d *Debugger ) hGetClient (machId string ) *Client {
if d .Clients == nil {
return nil
}
c , ok := d .Clients [machId ]
if !ok {
return nil
}
return c
}
func (d *Debugger ) hGetClientTx (
machId , txId string ,
) (*Client , *dbg .DbgMsgTx ) {
c := d .hGetClient (machId )
if c == nil {
return nil , nil
}
idx := c .TxIndex (txId )
if idx < 0 {
return nil , nil
}
tx := c .Tx (idx )
if tx == nil {
return nil , nil
}
return c , tx
}
func (d *Debugger ) Client () (*Client , error ) {
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
c , _ := amhelp .EvalGetter (nil , "Client" , 3 , d .Mach ,
func () (*Client , error ) {
return d .C , nil
})
if c == nil {
return nil , fmt .Errorf ("no client selected" )
}
return c , nil
}
func (d *Debugger ) NextTx () *dbg .DbgMsgTx {
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
tx , _ := amhelp .EvalGetter (d .Mach .Context (), "NextTx" , 3 , d .Mach ,
func () (*dbg .DbgMsgTx , error ) {
return d .hNextTx (), nil
})
return tx
}
func (d *Debugger ) hNextTx () *dbg .DbgMsgTx {
idx := d .hNextTxIdx ()
if idx < 0 {
return nil
}
return d .C .MsgTxs [idx ]
}
func (d *Debugger ) hNextTxIdx () int {
c := d .C
if c == nil {
return -1
}
return d .hFilterTxCursor1 (c , c .CursorTx1 +1 , false ) - 1
}
func (d *Debugger ) CurrentTx () *dbg .DbgMsgTx {
var tx *dbg .DbgMsgTx
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
d .Mach .Eval ("CurrentTx" , func () {
tx = d .hCurrentTx ()
}, nil )
return tx
}
func (d *Debugger ) hCurrentTx () *dbg .DbgMsgTx {
c := d .C
if c == nil {
return nil
}
if c .CursorTx1 == 0 || len (c .MsgTxs ) < c .CursorTx1 {
return nil
}
return c .MsgTxs [c .CursorTx1 -1 ]
}
func (d *Debugger ) hCurrentTxParsed () *types .MsgTxParsed {
c := d .C
if c == nil {
return nil
}
if c .CursorTx1 == 0 || len (c .MsgTxsParsed ) < c .CursorTx1 {
return nil
}
return d .C .MsgTxsParsed [d .C .CursorTx1 -1 ]
}
func (d *Debugger ) PrevTx () *dbg .DbgMsgTx {
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
tx , _ := amhelp .EvalGetter (nil , "PrevTx" , 3 , d .Mach ,
func () (*dbg .DbgMsgTx , error ) {
return d .hPrevTx (), nil
})
return tx
}
func (d *Debugger ) hPrevTx () *dbg .DbgMsgTx {
idx := d .hPrevTxIdx ()
if idx < 0 {
return nil
}
return d .C .MsgTxs [idx ]
}
func (d *Debugger ) hPrevTxIdx () int {
c := d .C
if c == nil {
return -1
}
return d .hFilterTxCursor1 (c , c .CursorTx1 -1 , true ) - 1
}
func (d *Debugger ) hConnectedClients () int {
var conns int
for _ , c := range d .Clients {
if c .Connected .Load () {
conns ++
}
}
return conns
}
func (d *Debugger ) MachAddr () *types .MachAddress {
addr , _ := amhelp .EvalGetter (d .Mach .Context (), "MachAddr" , 3 , d .Mach ,
func () (*types .MachAddress , error ) {
if d .C == nil {
return nil , fmt .Errorf ("no client selected" )
}
ret := &types .MachAddress {
MachId : d .C .Id ,
}
if d .C .CursorTx1 > 0 {
tx := d .C .MsgTxs [d .C .CursorTx1 -1 ]
ret .TxId = tx .ID
ret .MachTime = d .C .MsgTxsParsed [d .C .CursorTx1 -1 ].TimeSum
ret .HumanTime = *tx .Time
ret .QueueTick = tx .QueueTick
}
if d .C .CursorStep1 > 0 {
ret .Step = d .C .CursorStep1
}
if d .C .SelectedGroup != "" {
ret .Group = d .C .SelectedGroup
}
return ret , nil
})
return addr
}
func (d *Debugger ) Dispose () {
logger := d .params .DbgLogger
if logger != nil {
if file , ok := logger .Writer ().(*os .File ); ok {
file .Close ()
}
}
}
func (d *Debugger ) Start () {
d .Mach .Add1 (ss .Start , nil )
}
func (d *Debugger ) SetFilterLogLevel (lvl am .LogLevel ) {
d .Mach .Eval ("SetFilterLogLevel" , func () {
d .params .Filters .LogLevel = lvl
d .Mach .Add1 (ss .ToolToggled , nil )
d .hUpdateSchemaLogGrid ()
d .hRedrawFull (false )
}, nil )
}
func (d *Debugger ) hImportData (filename string ) {
var reader *bufio .Reader
u , err := url .Parse (filename )
if err == nil && u .Host != "" {
resp , err := http .Get (filename )
if err != nil {
d .Mach .AddErr (err , nil )
return
}
reader = bufio .NewReader (resp .Body )
} else {
fr , err := os .Open (filename )
if err != nil {
d .Mach .AddErr (err , nil )
return
}
defer fr .Close ()
reader = bufio .NewReader (fr )
}
brReader := brotli .NewReader (reader )
decoder := gob .NewDecoder (brReader )
var res []*server .Exportable
err = decoder .Decode (&res )
if err != nil {
d .Mach .AddErr (fmt .Errorf ("import failed: %w" , err ), nil )
return
}
for _ , data := range res {
id := data .MsgStruct .ID
hash := amhelp .SchemaHash ((*data ).MsgStruct .States )
d .Clients [id ] = newClient (id , id , hash , data )
if d .graph != nil {
err := d .graph .AddClient (data .MsgStruct )
if err != nil {
d .Mach .AddErr (fmt .Errorf ("import failed: %w" , err ), nil )
return
}
}
d .Mach .Add1 (ss .InitClient , Pass (&A {
Id : id ,
}))
for i := range data .MsgTxs {
d .hParseMsg (d .Clients [id ], i )
}
}
amgraph .AddErrGraph (nil , d .Mach ,
d .hUpdateGraphFile (nil ))
runtime .GC ()
}
func (d *Debugger ) hUpdateToolbar () {
f := fmt .Sprintf
for i , row := range d .toolbarItems {
focused := d .Mach .Is1 (ss .Toolbar1Focused )
switch i {
case 1 :
focused = d .Mach .Is1 (ss .Toolbar2Focused )
case 2 :
focused = d .Mach .Is1 (ss .Toolbar3Focused )
case 3 :
focused = d .Mach .Is1 (ss .Toolbar4Focused )
}
for ii , item := range row {
text := ""
_ , sel := d .toolbars [i ].GetSelection ()
esc := cview .Escape
if item .active != nil && item .active () {
if item .activeLabel != nil {
text += f (" [::b]%s[::-]" , esc ("[" +item .activeLabel ()+"]" ))
} else {
text += f (" [::b]%s[::-]" , esc ("[X]" ))
}
} else if item .active == nil && item .icon != "" {
text += f (" [" +theme .Grey +"]%s[-]%s[" +theme .Grey +"]%s[-]" ,
esc ("[" ), item .icon , esc ("]" ))
} else if item .active == nil {
text += f (" [" + theme .Grey + "][ ][-]" )
} else {
text += f (" [ ]" )
}
if sel != -1 && d .toolbarItems [i ][sel ].id == item .id && focused {
text += "[" + theme .White + "]" + item .label
} else if !focused {
text += f ("[%s]%s" , theme .Grey , item .label )
} else {
text += f ("%s" , item .label )
}
cell := d .toolbars [i ].GetCell (0 , ii )
cell .SetText (text )
cell .SetTextColor (tcell .GetColor (theme .White ))
d .toolbars [i ].SetCell (0 , ii , cell )
}
}
}
func (d *Debugger ) hUpdateAddressBar () {
machId := ""
machConn := false
txId := ""
stepId := ""
if d .C != nil {
machId = d .C .Id
if d .C .CursorTx1 > 0 {
txId = d .C .MsgTxs [d .C .CursorTx1 -1 ].ID
}
if d .C .CursorStep1 > 0 {
stepId = strconv .Itoa (d .C .CursorStep1 )
}
machConn = d .C .Connected .Load ()
}
copyCell := d .addressBar .GetCell (0 , colCopy )
copyCell .SetBackgroundColor (tcell .GetColor (theme .LightGrey ))
copyCell .SetTextColor (tcell .GetColor (theme .BgPrimary ))
if machId == "" {
copyCell .SetSelectable (false )
copyCell .SetBackgroundColor (tcell .ColorDefault )
} else {
copyCell .SetSelectable (true )
}
pasteCell := d .addressBar .GetCell (0 , colPaste )
pasteCell .SetTextColor (tcell .GetColor (theme .BgPrimary ))
pasteCell .SetBackgroundColor (tcell .GetColor (theme .LightGrey ))
fwdCell := d .addressBar .GetCell (0 , colNext )
fwdCell .SetBackgroundColor (tcell .GetColor (theme .LightGrey ))
fwdCell .SetTextColor (tcell .GetColor (theme .BgPrimary ))
fwdMachCell := d .addressBar .GetCell (0 , colNextMach )
fwdMachCell .SetBackgroundColor (tcell .GetColor (theme .LightGrey ))
fwdMachCell .SetTextColor (tcell .GetColor (theme .BgPrimary ))
fwdMachCell .SetSelectable (true )
if d .HistoryCursor > 0 {
fwdCell .SetSelectable (true )
} else {
fwdCell .SetSelectable (false )
fwdCell .SetTextColor (tcell .GetColor (theme .Grey ))
fwdCell .SetBackgroundColor (tcell .ColorDefault )
}
nextMach := false
for i := d .HistoryCursor ; i > 0 ; i -- {
if d .History [i ].MachId != d .History [i -1 ].MachId {
nextMach = true
break
}
}
if !nextMach {
fwdMachCell .SetSelectable (false )
fwdMachCell .SetTextColor (tcell .GetColor (theme .Grey ))
fwdMachCell .SetBackgroundColor (tcell .ColorDefault )
}
backCell := d .addressBar .GetCell (0 , colPrev )
backCell .SetBackgroundColor (tcell .GetColor (theme .LightGrey ))
backCell .SetTextColor (tcell .GetColor (theme .BgPrimary ))
backMachCell := d .addressBar .GetCell (0 , colPrevMach )
backMachCell .SetBackgroundColor (tcell .GetColor (theme .LightGrey ))
backMachCell .SetTextColor (tcell .GetColor (theme .BgPrimary ))
backMachCell .SetSelectable (true )
if d .HistoryCursor < len (d .History )-1 {
backCell .SetSelectable (true )
} else {
backCell .SetSelectable (false )
backCell .SetTextColor (tcell .GetColor (theme .Grey ))
backCell .SetBackgroundColor (tcell .ColorDefault )
}
prevMach := false
for i := d .HistoryCursor ; i < len (d .History )-1 ; i ++ {
if d .History [i ].MachId != d .History [i +1 ].MachId {
prevMach = true
break
}
}
if !prevMach {
backMachCell .SetSelectable (false )
backMachCell .SetTextColor (tcell .GetColor (theme .Grey ))
backMachCell .SetBackgroundColor (tcell .ColorDefault )
}
if d .clip == nil {
copyCell .SetTextColor (tcell .GetColor (theme .Grey ))
copyCell .SetSelectable (false )
copyCell .SetBackgroundColor (tcell .ColorDefault )
pasteCell .SetTextColor (tcell .GetColor (theme .Grey ))
pasteCell .SetSelectable (false )
pasteCell .SetBackgroundColor (tcell .ColorDefault )
}
machColor := "[" + theme .Grey + "]"
if machConn {
machColor = "[" + theme .Active + "]"
}
addrCell := d .addressBar .GetCell (0 , colAddr )
if machId != "" && txId != "" {
s := ""
if stepId != "" {
s = "/" + stepId
}
addrCell .SetText (machColor + "mach://[-][::u]" + machId +
"[::-][" + theme .Grey + "]/" + txId + s )
} else if machId != "" {
addrCell .SetText (machColor + "mach://[-][::u]" + machId )
} else {
addrCell .SetText ("[" + theme .Grey + "]mach://[-]" )
}
tags := ""
if machId != "" {
if len (d .C .MsgStruct .Tags ) > 0 {
tags += "[::b]#[::-]" + strings .Join (d .C .MsgStruct .Tags , " [::b]#[::-]" )
}
parentTags := d .hGetParentTags (d .C , nil )
if len (parentTags ) > 0 {
if tags != "" {
tags += " ... "
}
tags += "[::b]#[::-]" + strings .Join (parentTags , " [::b]#[::-]" )
}
}
d .tagsBar .SetText (tags )
}
func (d *Debugger ) hUpdateViews (immediate bool ) {
if d .contentPanels == nil {
return
}
switch d .Mach .Switch (states .DebuggerGroups .Views ) {
case ss .MatrixView :
d .hUpdateMatrix ()
d .contentPanels .HidePanel ("tree-log" )
d .contentPanels .HidePanel ("tree-matrix" )
d .contentPanels .ShowPanel ("matrix" )
case ss .TreeMatrixView :
d .hUpdateMatrix ()
d .hUpdateSchemaTree ()
d .contentPanels .HidePanel ("matrix" )
d .contentPanels .HidePanel ("tree-log" )
d .contentPanels .ShowPanel ("tree-matrix" )
case ss .TreeLogView :
fallthrough
default :
d .hUpdateSchemaTree ()
if immediate {
d .Mach .Add1 (ss .UpdateLogScheduled , nil )
} else {
d .Mach .Add1 (ss .UpdateLogScheduled , nil )
}
d .contentPanels .HidePanel ("matrix" )
d .contentPanels .HidePanel ("tree-matrix" )
d .contentPanels .ShowPanel ("tree-log" )
}
}
func (d *Debugger ) hParseMsg (c *Client , idx int ) {
msgTx := c .MsgTxs [idx ]
var sum uint64
for _ , v := range msgTx .Clocks {
sum += v
}
index := c .MsgStruct .StatesIndex
prevTx := &dbg .DbgMsgTx {}
prevTxParsed := &types .MsgTxParsed {}
if len (c .MsgTxs ) > 1 && idx > 0 {
prevTx = c .MsgTxs [idx -1 ]
prevTxParsed = c .MsgTxsParsed [idx -1 ]
}
fakeTx := &am .Transition {
TimeBefore : prevTx .Clocks ,
TimeAfter : msgTx .Clocks ,
Steps : msgTx .Steps ,
}
after := fakeTx .TimeAfter .Sum (nil )
before := fakeTx .TimeBefore .Sum (nil )
if after < before {
d .Mach .AddErr (fmt .Errorf ("time after < time before" ), nil )
c .MTimeSum = sum
c .MsgTxsParsed = append (c .MsgTxsParsed , &types .MsgTxParsed {TimeSum : sum })
c .LogMsgs = append (c .LogMsgs , make ([]*am .LogEntry , 0 ))
return
}
added , removed , touched := amhelp .GetTransitionStates (fakeTx , index )
msgTxParsed := &types .MsgTxParsed {
TimeSum : sum ,
TimeDiff : sum - prevTxParsed .TimeSum ,
StatesAdded : c .StatesToIndexes (added ),
StatesRemoved : c .StatesToIndexes (removed ),
StatesTouched : c .StatesToIndexes (touched ),
}
if len (msgTx .CalledStates ) > 0 {
msgTx .CalledStatesIdxs = amhelp .StatesToIndexes (index ,
msgTx .CalledStates )
msgTx .CalledStates = nil
}
for _ , step := range msgTx .Steps {
if step .FromState != "" || step .ToState != "" {
step .FromStateIdx = slices .Index (index , step .FromState )
step .ToStateIdx = slices .Index (index , step .ToState )
step .FromState = ""
step .ToState = ""
}
if step .Data != nil {
step .RelType , _ = step .Data .(am .Relation )
}
}
var isErr bool
for _ , name := range index {
if strings .HasPrefix (name , am .PrefixErr ) && msgTx .Is1 (index , name ) {
isErr = true
break
}
}
if isErr || msgTx .Is1 (index , am .StateException ) {
c .Errors = append ([]int {idx }, c .Errors ...)
}
c .MsgTxsParsed = append (c .MsgTxsParsed , msgTxParsed )
c .MTimeSum = sum
d .hParseMsgLog (c , msgTx , idx )
if d .graph != nil {
d .graph .ParseMsg (c .Id , msgTx )
}
if d .Mach .Is1 (ss .Start ) {
d .Mach .Add1 (ss .BuildingLog , nil )
}
msgTx .CalledStates = amhelp .IndexesToStates (index , msgTx .CalledStatesIdxs )
if d .params .OutputCallLog && len (msgTx .Steps ) > 0 {
if err := d .appendCallLog (c , msgTx , msgTxParsed ); err != nil {
d .Mach .AddErr (err , nil )
}
}
}
func (d *Debugger ) appendCallLog (
c *Client , msgTx *dbg .DbgMsgTx , msgTxParsed *types .MsgTxParsed ,
) error {
steps := ""
stepsCount := 0
sepLines := 100
rotateCount := 5000
negOpened := false
indent := "\t"
for _ , entry := range msgTx .LogEntries {
if !strings .HasPrefix (entry .Text , "[handler:" ) {
continue
}
idx := strings .Index (entry .Text , "]" )
if idx == -1 {
continue
}
handlerIdx := entry .Text [len ("[handler:" ):idx ]
name := entry .Text [idx +2 :]
if strings .HasPrefix (name , am .StateAny ) ||
strings .HasPrefix (name , am .StateHealthcheck ) ||
strings .HasPrefix (name , am .StateHeartbeat ) {
continue
}
if (strings .HasSuffix (name , am .SuffixEnter ) ||
strings .HasSuffix (name , am .SuffixExit )) && !negOpened {
negOpened = true
steps += "\t{\n"
indent += "\t"
} else if (strings .HasSuffix (name , am .SuffixState ) ||
strings .HasSuffix (name , am .SuffixEnd )) && negOpened {
negOpened = false
steps += "\t}\n"
indent = "\t"
}
if negOpened && !msgTx .Accepted {
steps += fmt .Sprintf ("%sh%s.%s(e) // => am.Canceled\n" ,
indent , handlerIdx , name )
} else {
steps += fmt .Sprintf ("%sh%s.%s(e)\n" , indent , handlerIdx , name )
}
stepsCount ++
}
if negOpened {
steps += "\t}\n"
}
if steps == "" {
return nil
}
steps = P .Sprintf ("\t// t%v\n%s" , msgTxParsed .TimeSum , steps )
dir := path .Join (d .params .OutputDir , "call-log" , c .Id )
if file , ok := d .callLogFiles [c .Id ]; !ok {
err := os .MkdirAll (dir , 0o755 )
if err != nil {
return err
}
if err := d .callLogCleanup (dir ); err != nil {
return err
}
if err := d .callLogBootstrap (dir ); err != nil {
return err
}
file , err := os .Create (path .Join (dir , "0.go" ))
if err != nil {
return err
}
d .callLogFiles [c .Id ] = file
content := callStepsToContent ("0" , steps )
d .callLogFilesLen [c .Id ] = int64 (len (content ))
if _, err = file .Write (content ); err != nil {
return err
}
d .callLogCount [c .Id ] = stepsCount
} else if ok && d .callLogCount [c .Id ] > rotateCount {
name := strconv .FormatUint (msgTxParsed .TimeSum , 10 )
file , err := os .Create (path .Join (dir ,
name +".go" ))
if err != nil {
return err
}
d .callLogFiles [c .Id ] = file
content := callStepsToContent (name , steps )
d .callLogFilesLen [c .Id ] = int64 (len (content ))
if _, err = file .Write (content ); err != nil {
return err
}
d .callLogCount [c .Id ] = stepsCount
} else {
if d .callLogLastSep [c .Id ] < d .callLogCount [c .Id ]-sepLines {
steps += "\n\tactive = am.S{"
active := msgTx .ActiveStates (c .MsgStruct .StatesIndex )
for i , name := range active {
if i > 0 {
steps += ", "
}
steps += "ss." + name
}
steps += "}\n"
d .callLogLastSep [c .Id ] = d .callLogCount [c .Id ] + stepsCount
}
code := utils .Sp (`
%s
}
` , steps )
content := []byte (code )
if _ , err := file .WriteAt (content , d .callLogFilesLen [c .Id ]-3 ); err != nil {
return err
}
d .callLogFilesLen [c .Id ] += int64 (len (content ) - 3 )
d .callLogCount [c .Id ] += stepsCount
}
return nil
}
func callStepsToContent(name string , steps string ) []byte {
code := utils .Sp (`
// Code generated by am-dbg, not for execution. Edit init.go for type defs.
package main
// Omitted: Any*(), Heartbeat*(), Healthcheck*()
func callLog%s() {
%s
}
` , name , steps )
return []byte (code )
}
func (d *Debugger ) callLogCleanup (dir string ) error {
entries , err := os .ReadDir (dir )
if err != nil {
return err
}
for _ , entry := range entries {
if entry .IsDir () {
continue
}
fileName := entry .Name ()
if !strings .HasSuffix (fileName , "init.go" ) {
fullPath := filepath .Join (dir , fileName )
if err := os .Remove (fullPath ); err != nil {
return err
}
}
}
return nil
}
func (d *Debugger ) hIsTxSkipped (c *Client , idx int ) bool {
if !d .filtersActive () {
return false
}
return slices .Index (c .MsgTxsFiltered , idx ) == -1
}
func (d *Debugger ) hFilterTxCursor1 (c *Client , newCursor1 int , back bool ) int {
if !d .filtersActive () {
return newCursor1
}
for {
if newCursor1 < 1 {
return 0
} else if newCursor1 > len (c .MsgTxs ) {
if !d .hIsTxSkipped (c , c .CursorTx1 -1 ) {
return c .CursorTx1
} else {
return 0
}
}
if d .hIsTxSkipped (c , newCursor1 -1 ) {
if back {
newCursor1 --
} else {
newCursor1 ++
}
} else {
break
}
}
return newCursor1
}
func (d *Debugger ) hUpdateTxBars () {
d .currTxBarLeft .Clear ()
d .currTxBarRight .Clear ()
d .nextTxBarLeft .Clear ()
d .nextTxBarRight .Clear ()
if d .Mach .Not (am .S {ss .SelectingClient , ss .ClientSelected }) {
d .currTxBarLeft .SetText ("Listening for connections on " + d .params .AddrRpc )
return
}
c := d .C
tx := d .hCurrentTx ()
if tx == nil {
if c == nil || len (c .MsgTxs ) == 0 {
d .currTxBarLeft .SetText ("No transitions yet..." )
} else {
d .currTxBarLeft .SetText ("Initial machine schema" )
}
} else {
var title string
switch d .Mach .Switch (states .DebuggerGroups .Playing ) {
case ss .Playing :
title = formatTxBarTitle ("Playing" )
case ss .TailMode :
title += formatTxBarTitle ("Tail" ) + " "
default :
title = formatTxBarTitle ("Paused" ) + " "
}
left , right := d .hGetTxInfo (c .CursorTx1 -1 , title )
d .currTxBarLeft .SetText (left )
d .currTxBarRight .SetText (right )
}
nextTxIdx := d .hNextTxIdx ()
if nextTxIdx > 0 && c != nil {
title := "Next "
left , right := d .hGetTxInfo (nextTxIdx , title )
d .nextTxBarLeft .SetText (left )
d .nextTxBarRight .SetText (right )
}
}
func (d *Debugger ) hUpdateTimelines () {
c := d .C
if c == nil {
return
}
txCount := len (c .MsgTxs )
nextTx := d .hNextTx ()
d .timelineSteps .SetTitleColor (cview .Styles .PrimaryTextColor )
d .timelineSteps .SetFilledColor (cview .Styles .PrimaryTextColor )
if nextTx != nil && !nextTx .Accepted {
d .timelineSteps .SetFilledColor (tcell .GetColor (theme .Grey ))
}
if nextTx != nil && c .CursorStep1 == len (nextTx .Steps ) && !nextTx .Accepted {
d .timelineSteps .SetFilledColor (tcell .GetColor (theme .Err ))
}
stepsCount := 0
if nextTx != nil {
stepsCount = len (nextTx .Steps )
}
d .timelineTxs .SetMax (max (txCount , 1 ))
d .timelineTxs .SetProgress (c .CursorTx1 )
var title string
if d .filtersActive () {
pos := slices .Index (c .MsgTxsFiltered , c .CursorTx1 -1 ) + 1
if c .CursorTx1 == 0 {
pos = 0
}
title = P .Sprintf (" Transition %d / %d [%s]%d / %d[-] " ,
pos , len (c .MsgTxsFiltered ), theme .Grey , c .CursorTx1 , txCount )
} else {
title = P .Sprintf (" Transition %d / %d " , c .CursorTx1 , txCount )
}
d .timelineTxs .SetTitle (title )
d .timelineTxs .SetEmptyRune (' ' )
d .timelineSteps .SetMax (max (stepsCount , 1 ))
d .timelineSteps .SetProgress (c .CursorStep1 )
d .timelineSteps .SetTitle (fmt .Sprintf (
" Next mutation step %d / %d " , c .CursorStep1 , stepsCount ,
))
d .timelineSteps .SetEmptyRune (' ' )
}
func (d *Debugger ) hUpdateBorderColor () {
colorStr := theme .Inactive
if d .Mach .IsErr () {
colorStr = theme .Err
}
color := tcell .GetColor (colorStr )
for _ , box := range d .focusable {
box .SetBorderColorFocused (color )
}
}
func (d *Debugger ) hExportData (filename string , snapshot bool ) {
if filename == "" {
log .Printf ("Error: export failed no filename" )
return
}
if len (d .Clients ) == 0 {
log .Printf ("Error: export failed no clients" )
return
}
gobPath := path .Join (d .params .OutputDir , filename +".gob.br" )
fw , err := os .Create (gobPath )
if err != nil {
log .Printf ("Error: export failed %s" , err )
return
}
defer fw .Close ()
now := time .Now ()
if d .PrevTx () != nil {
now = *d .C .Tx (max (0 , d .C .CursorTx1 -1 )).Time
}
data := make ([]*server .Exportable , 0 , len (d .Clients ))
i := 0
for _ , c := range d .Clients {
if d .Mach .Is1 (ss .FilterDisconn ) && !c .Connected .Load () {
continue
}
if d .Mach .Is1 (ss .FilterRpcMachs ) && machIsRpc (c .MsgStruct ) {
continue
}
data = append (data , &server .Exportable {
MsgStruct : c .Exportable .MsgStruct ,
MsgTxs : c .Exportable .MsgTxs ,
Version : utils .GetVersion (),
})
if snapshot {
data [i ].MsgTxs = []*dbg .DbgMsgTx {c .Tx (c .TxAtHTime (now ))}
}
i ++
}
brCompress := brotli .NewWriter (fw )
defer brCompress .Close ()
encoder := gob .NewEncoder (brCompress )
err = encoder .Encode (data )
if err != nil {
log .Printf ("Error: export failed %s" , err )
}
}
func (d *Debugger ) hGetTxInfo (txIdx int , title string ) (string , string ) {
tx := d .C .MsgTxs [txIdx ]
parsed := d .C .MsgTxsParsed [txIdx ]
left := title
right := " "
if tx == nil {
return left , right
}
var prev *dbg .DbgMsgTx
prevIdx := d .hFilterTxCursor1 (d .C , txIdx , true ) - 1
if prevIdx > 0 {
prev = d .C .MsgTxs [prevIdx ]
}
calledStates := tx .CalledStateNames (d .C .MsgStruct .StatesIndex )
left += P .Sprintf (" | tx: %d" , txIdx )
if parsed .TimeDiff == 0 {
left += " | Time: [" + theme .Grey + "] 0[-]"
} else {
left += P .Sprintf (" | Time: +%d" , parsed .TimeDiff )
}
left += " |"
multi := ""
if len (calledStates ) == 1 && d .C .MsgStruct .States [calledStates [0 ]].Multi {
multi += " multi"
}
if !tx .Accepted {
left += "[" + theme .Grey + "]"
}
queued := ""
if tx .IsQueued {
queued = "q"
}
left += fmt .Sprintf (" %s%s%s: [::b]%s[::-]" , queued , tx .Type , multi ,
strings .Join (calledStates , ", " ))
if !tx .Accepted {
left += "[-]"
}
if tx .IsAuto {
right += "auto | "
}
if tx .IsCheck {
right += "check | "
}
if !tx .Accepted {
right += "[" + theme .Grey + "]canceled[-] | "
}
tStamp := tx .Time .Format (timeFormat )
if prev != nil {
prevTStamp := prev .Time .Format (timeFormat )
if idx := findFirstDiff (prevTStamp , tStamp ); idx != -1 {
tStamp = tStamp [:idx ] + "[" + theme .White + "]" +
tStamp [idx :idx +1 ] + "[" + theme .Grey + "]" + tStamp [idx +1 :]
}
}
right += fmt .Sprintf (
"add: %d | rm: %d | touch: %3s | [" +theme .Grey +"]%s" ,
len (parsed .StatesAdded ), len (parsed .StatesRemoved ),
strconv .Itoa (len (parsed .StatesTouched )), tStamp ,
)
return left , right
}
func (d *Debugger ) hCleanOnConnect () bool {
if len (d .Clients ) == 0 {
return false
}
var disconns []*Client
for _ , c := range d .Clients {
if !c .Connected .Load () {
disconns = append (disconns , c )
}
}
if len (disconns ) == len (d .Clients ) {
for _ , c := range d .Clients {
d .hRemoveClient (c .Id )
}
if d .graph != nil {
d .graph .Clear ()
}
return true
}
return false
}
func (d *Debugger ) hUpdateMatrix () {
if !d .Mach .Any1 (ss .MatrixView , ss .TreeMatrixView ) {
return
}
if d .Mach .Is1 (ss .MatrixRain ) {
d .hUpdateMatrixRain ()
} else {
d .hUpdateMatrixRelations ()
}
}
func (d *Debugger ) hUpdateMatrixRelations () {
d .matrix .Clear ()
d .matrix .SetTitle (" Matrix " )
c := d .C
if c == nil || d .C .CursorTx1 == 0 {
return
}
index := c .MsgStruct .StatesIndex
if c .SelectedGroup != "" {
index = c .MsgSchemaParsed .Groups [c .SelectedGroup ]
}
var tx *dbg .DbgMsgTx
var prevTx *dbg .DbgMsgTx
if c .CursorStep1 == 0 {
tx = d .hCurrentTx ()
prevTx = d .hPrevTx ()
} else {
tx = d .hNextTx ()
prevTx = d .hCurrentTx ()
}
steps := tx .Steps
calledStates := tx .CalledStateNames (c .MsgStruct .StatesIndex )
if c .CursorStep1 > 0 {
steps = steps [:c .CursorStep1 ]
}
highlightIndex := -1
var called []int
for i , name := range index {
v := "0"
if slices .Contains (calledStates , name ) {
v = "1"
called = append (called , i )
}
d .matrix .SetCellSimple (0 , i , matrixCellVal (v ))
if slices .Contains (calledStates , name ) {
d .matrix .GetCell (0 , i ).SetAttributes (tcell .AttrBold | tcell .AttrUnderline )
}
if d .C .SelectedState == name {
d .matrix .GetCell (0 , i ).SetBackgroundColor (
tcell .GetColor (theme .Highlight3 ),
)
highlightIndex = i
}
}
matrixEmptyRow (d , 1 , len (index ), highlightIndex )
sum := 0
for i , name := range index {
var pTick uint64
if prevTx != nil {
pTick = prevTx .Clock (index , name )
}
tick := tx .Clock (index , name )
v := tick - pTick
sum += int (v )
d .matrix .SetCellSimple (2 , i , matrixCellVal (strconv .Itoa (int (v ))))
cell := d .matrix .GetCell (2 , i )
if v == 0 {
cell .SetTextColor (tcell .GetColor (theme .Grey ))
}
if slices .Contains (called , i ) {
cell .SetAttributes (
tcell .AttrBold | tcell .AttrUnderline ,
)
}
if d .C .SelectedState == name {
cell .SetBackgroundColor (tcell .GetColor (theme .Highlight3 ))
}
}
matrixEmptyRow (d , 3 , len (index ), highlightIndex )
for iRow , target := range index {
for iCol , source := range index {
v := 0
for _ , step := range steps {
if step .GetFromState (c .MsgStruct .StatesIndex ) == source &&
((step .ToStateIdx == -1 && source == target ) ||
step .GetToState (c .MsgStruct .StatesIndex ) == target ) {
v += int (step .Type )
}
strVal := strconv .Itoa (v )
strVal = matrixCellVal (strVal )
d .matrix .SetCellSimple (iRow +4 , iCol , strVal )
cell := d .matrix .GetCell (iRow +4 , iCol )
if d .C .SelectedState == target || d .C .SelectedState == source {
cell .SetBackgroundColor (tcell .GetColor (theme .Highlight3 ))
}
if v == 0 {
cell .SetTextColor (tcell .GetColor (theme .Grey ))
continue
}
if slices .Contains (called , iRow ) || slices .Contains (called , iCol ) {
cell .SetAttributes (tcell .AttrBold | tcell .AttrUnderline )
} else {
cell .SetAttributes (tcell .AttrBold )
}
}
}
}
title := " Matrix:" + strconv .Itoa (sum ) + " "
if c .CursorTx1 > 0 {
t := strconv .Itoa (int (c .MsgTxsParsed [c .CursorTx1 -1 ].TimeSum ))
title += "Time:t" + t + " "
}
d .matrix .SetTitle (title )
}
func (d *Debugger ) hUpdateMatrixRain () {
if d .Mach .Not1 (ss .MatrixRain ) {
return
}
d .matrix .Clear ()
d .matrix .SetTitle (" Rain " )
c := d .C
if c == nil {
return
}
currTxRow := -1
d .matrix .SetSelectionChangedFunc (func (row , column int ) {
d .Mach .Add1 (ss .MatrixRainSelected , Pass (&A {
Row : row ,
Column : column ,
CurrTxRow : currTxRow ,
}))
})
d .matrix .SetSelectable (true , true )
index := c .MsgStruct .StatesIndex
if g := c .SelectedGroup ; g != "" {
index = c .MsgSchemaParsed .Groups [g ]
}
tx := d .hCurrentTx ()
prevTx := d .hPrevTx ()
_ , _ , _ , height := d .matrix .GetInnerRect ()
height -= 1
toShow := []int {}
ahead := height / 2
if d .Mach .Is1 (ss .TailMode ) {
ahead = 0
}
cur := c .FilterIndexByCursor1 (c .CursorTx1 )
var curLast int
aheadOk := func (i int , max int ) bool {
return i < len (c .MsgTxsFiltered ) && len (toShow ) <= max
}
for i := cur ; aheadOk (i , ahead ); i ++ {
toShow = append (toShow , c .MsgTxsFiltered [i ])
curLast = i
}
behindOk := func (i int ) bool {
return i >= 0 && i < len (c .MsgTxsFiltered ) && len (toShow ) <= height
}
for i := cur - 1 ; behindOk (i ); i -- {
toShow = slices .Concat ([]int {c .MsgTxsFiltered [i ]}, toShow )
}
for i := curLast + 1 ; aheadOk (i , height ); i ++ {
toShow = append (toShow , c .MsgTxsFiltered [i ])
}
for rowIdx , txIdx1 := range toShow {
row := ""
txIdx1 = txIdx1 + 1
if txIdx1 == c .CursorTx1 {
currTxRow = rowIdx
}
tx := c .MsgTxs [txIdx1 -1 ]
txParsed := c .MsgTxsParsed [txIdx1 -1 ]
calledStates := tx .CalledStateNames (c .MsgStruct .StatesIndex )
for ii , name := range index {
v := "."
sIsErr := strings .HasPrefix (name , "Err" )
if tx .Is1 (index , name ) {
v = "1"
if slices .Contains (txParsed .StatesTouched , ii ) {
v = "2"
}
} else if slices .Contains (txParsed .StatesRemoved , ii ) {
v = "|"
} else if !tx .Accepted && slices .Contains (calledStates , index [ii ]) {
v = "c"
} else if slices .Contains (txParsed .StatesTouched , ii ) {
v = "*"
}
row += v
d .matrix .SetCellSimple (rowIdx , ii , v )
cell := d .matrix .GetCell (rowIdx , ii )
cell .SetSelectable (true )
if !tx .Accepted || v == "." || v == "|" || v == "c" || v == "*" {
cell .SetTextColor (tcell .GetColor (theme .Highlight ))
}
if slices .Contains (calledStates , name ) {
cell .SetAttributes (tcell .AttrUnderline )
}
if txIdx1 == c .CursorTx1 {
cell .SetBackgroundColor (tcell .GetColor (theme .Highlight3 ))
} else if d .C .SelectedState == name {
cell .SetBackgroundColor (tcell .GetColor (theme .Highlight3 ))
}
if (sIsErr || name == am .StateException ) && tx .Is1 (index , name ) {
if tx .Accepted {
cell .SetBackgroundColor (tcell .GetColor (theme .ErrBg ))
} else {
cell .SetBackgroundColor (tcell .GetColor (theme .Highlight3 ))
}
}
}
tStamp := tx .Time .Format (timeFormat )
tStampFmt := tStamp
if txIdx1 > 1 {
prevTStamp := c .MsgTxs [txIdx1 -2 ].Time .Format (timeFormat )
if idx := findFirstDiff (prevTStamp , tStamp ); idx != -1 {
tStampFmt = tStamp [:idx ] + "[" + theme .White + "]" +
tStamp [idx :idx +1 ] + "[" + theme .Grey + "]" + tStamp [idx +1 :]
}
}
d .matrix .SetCellSimple (rowIdx , len (index ), fmt .Sprintf (
" [" +theme .Grey +"]%d | %s[-]" , txIdx1 , tStampFmt ,
))
tailCell := d .matrix .GetCell (rowIdx , len (index ))
if txIdx1 == c .CursorTx1 {
tailCell .SetBackgroundColor (tcell .GetColor (theme .Highlight3 ))
}
}
diffT := 0
if c .CursorTx1 > 0 {
for _ , name := range index {
var pTick uint64
if prevTx != nil {
pTick = prevTx .Clock (index , name )
}
tick := tx .Clock (index , name )
v := tick - pTick
diffT += int (v )
}
}
title := " Matrix:" + strconv .Itoa (diffT ) + " "
if c .CursorTx1 > 0 {
t := strconv .Itoa (int (c .MsgTxsParsed [c .CursorTx1 -1 ].TimeSum ))
title += "Time:t" + t + " "
}
d .matrix .SetTitle (title )
if d .Mach .Is1 (ss .TailMode ) {
d .matrix .ScrollToEnd ()
}
}
var spinnerFrames = []string {"⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" }
func (d *Debugger ) hUpdateStatusBar () {
d .statusBarLeft .SetText ("" )
d .statusBarRight .SetText ("" )
c := d .C
if c == nil {
return
}
tx := d .hCurrentTx ()
loading := " "
if d .Mach .Is1 (ss .Loading ) {
loading = spinnerFrames [d .loadingPos ]
}
var graphMTime uint64
var currHTime time .Time
currSelTx := d .hCurrentTx ()
if currSelTx != nil {
currHTime = d .lastScrolledTxTime
if currHTime .IsZero () {
currHTime = *currSelTx .Time
}
}
for _ , client := range d .Clients {
currCTxIdx := client .TxAtHTime (currHTime )
if currCTxIdx != -1 {
parsed := client .MsgTxsParsed [currCTxIdx ]
graphMTime += parsed .TimeSum
}
}
left := []string {P .Sprintf ("%sGraph:t%v" , loading , graphMTime )}
idx := slices .Index (c .MsgStruct .StatesIndex , c .SelectedState )
if idx != -1 {
left = append (left , "[::b]" +c .SelectedState +"[::-]" ,
fmt .Sprintf ("idx: %d" , idx ))
if tx != nil && len (tx .Clocks ) > idx {
left = append (left , fmt .Sprintf ("tick: %d" , tx .Clocks [idx ]))
}
}
d .statusBarLeft .SetText (strings .Join (left , " [" +theme .Grey +"]|[-] " ))
txt := ""
if c .CursorStep1 > 0 {
nextTx := d .hNextTx ()
if nextTx != nil && nextTx .Steps != nil {
stepIdx := min (len (nextTx .Steps )-1 , c .CursorStep1 -1 )
step := nextTx .Steps [stepIdx ]
txt = step .StringFromIndex (c .MsgStruct .StatesIndex )
}
}
i := 0
for strings .Contains (txt , "**" ) {
rep := "[::b]"
if i %2 == 1 {
rep = "[::-]"
}
i ++
txt = strings .Replace (txt , "**" , rep , 1 )
}
d .statusBarRight .SetText (txt )
}
func (d *Debugger ) hGetSidebarCurrClientIdx () int {
if d .C == nil {
return -1
}
i := 0
for _ , item := range d .clientList .GetItems () {
ref := item .GetReference ().(*sidebarRef )
if ref .name == d .C .Id {
return i
}
i ++
}
return -1
}
func (d *Debugger ) hFilterClientTxs () {
if d .C == nil || !d .filtersActive () {
return
}
d .C .MsgTxsFiltered = nil
for i := range d .C .MsgTxs {
match := d .hFilterTx (d .C , i , d .filtersFromStates ())
if match {
d .C .MsgTxsFiltered = append (d .C .MsgTxsFiltered , i )
}
}
}
func (d *Debugger ) filtersFromStates () *types .Filters {
is := d .Mach .Is1
return &types .Filters {
SkipCanceledTx : is (ss .FilterCanceledTx ),
SkipAutoTx : is (ss .FilterAutoTx ),
SkipAutoCanceledTx : is (ss .FilterAutoCanceledTx ),
SkipEmptyTx : is (ss .FilterEmptyTx ),
SkipHealthTx : is (ss .FilterHealth ),
SkipQueuedTx : is (ss .FilterQueuedTx ),
SkipOutGroup : is (ss .FilterOutGroup ),
SkipChecks : is (ss .FilterChecks ),
SkipRpcMach : is (ss .FilterRpcMachs ),
}
}
func (d *Debugger ) statesFromFilters (filters *types .Filters ) {
add := d .Mach .Add1
rm := d .Mach .Remove1
if filters .SkipCanceledTx {
add (ss .FilterCanceledTx , nil )
} else {
rm (ss .FilterCanceledTx , nil )
}
if filters .SkipAutoTx {
add (ss .FilterAutoTx , nil )
} else {
rm (ss .FilterAutoTx , nil )
}
if filters .SkipAutoCanceledTx {
add (ss .FilterAutoCanceledTx , nil )
} else {
rm (ss .FilterAutoCanceledTx , nil )
}
if filters .SkipEmptyTx {
add (ss .FilterEmptyTx , nil )
} else {
rm (ss .FilterEmptyTx , nil )
}
if filters .SkipHealthTx {
add (ss .FilterHealth , nil )
} else {
rm (ss .FilterHealth , nil )
}
if filters .SkipQueuedTx {
add (ss .FilterQueuedTx , nil )
} else {
rm (ss .FilterQueuedTx , nil )
}
if filters .SkipOutGroup {
add (ss .FilterOutGroup , nil )
} else {
rm (ss .FilterOutGroup , nil )
}
if filters .SkipChecks {
add (ss .FilterChecks , nil )
} else {
rm (ss .FilterChecks , nil )
}
if filters .SkipRpcMach {
add (ss .FilterRpcMachs , nil )
} else {
rm (ss .FilterRpcMachs , nil )
}
}
func (d *Debugger ) filtersActive () bool {
return d .Mach .Any1 (states .DebuggerGroups .Filters ...)
}
func (d *Debugger ) hFilterTx (c *Client , idx int , filters *types .Filters ) bool {
tx := c .MsgTxs [idx ]
parsed := c .MsgTxsParsed [idx ]
called := tx .CalledStateNames (c .MsgStruct .StatesIndex )
group := c .SelectedGroup
f := filters
if f .SkipAutoTx && tx .IsAuto {
return false
} else if f .SkipAutoCanceledTx && tx .IsAuto && !tx .Accepted {
return false
} else if f .SkipAutoCanceledTx && tx .IsAuto && tx .IsQueued {
executed := c .TxExecutedBy (idx )
if executed != nil && !executed .Accepted {
return false
}
}
if f .SkipCanceledTx && !tx .Accepted {
return false
}
if f .SkipQueuedTx && tx .IsQueued {
return false
}
if f .SkipChecks && tx .IsCheck {
return false
}
if f .SkipOutGroup && group != "" {
groupStates := c .MsgSchemaParsed .Groups [group ]
if len (am .StatesShared (called , groupStates )) == 0 {
return false
}
}
if f .SkipEmptyTx && parsed .TimeDiff == 0 && !tx .IsQueued && tx .Accepted {
return false
}
if f .SkipHealthTx {
health := S {ssam .BasicStates .Healthcheck , ssam .BasicStates .Heartbeat }
if len (called ) == 1 && slices .Contains (health , called [0 ]) {
return false
}
}
return true
}
func (d *Debugger ) hScrollToTime (
e *am .Event , hTime time .Time , filter bool ,
) bool {
if d .C == nil {
return false
}
latestTx := d .C .TxAtHTime (hTime )
if latestTx == -1 {
return false
}
if filter {
latestTx = d .hFilterTxCursor1 (d .C , latestTx , true )
}
d .hSetCursor1 (e , &A {
Cursor1 : latestTx ,
FilterBack : true ,
})
return true
}
func (d *Debugger ) hGetParentTags (c *Client , tags []string ) []string {
parent , ok := d .Clients [c .MsgStruct .Parent ]
if !ok {
return tags
}
tags = slices .Concat (tags , parent .MsgStruct .Tags )
return d .hGetParentTags (parent , tags )
}
func (d *Debugger ) hSyncOptsTimelines () {
switch d .params .ViewTimelines {
case types .ParamsViewTimelinesNone :
d .Mach .Add (S {ss .TimelineTxHidden , ss .TimelineStepsHidden }, nil )
case types .ParamsViewTimelinesOne :
d .Mach .Add1 (ss .TimelineStepsHidden , nil )
d .Mach .Remove1 (ss .TimelineTxHidden , nil )
case types .ParamsViewTimelinesTwo :
d .Mach .Remove (S {ss .TimelineStepsHidden , ss .TimelineTxHidden }, nil )
}
}
func (d *Debugger ) getFocusColor () tcell .Color {
color := cview .Styles .MoreContrastBackgroundColor
if d .Mach .IsErr () {
color = tcell .GetColor (theme .Err )
}
return color
}
func (d *Debugger ) LogReaderText () string {
ctx := d .Mach .NewStateCtx (ss .LogReaderVisible )
ret , _ := amhelp .EvalGetter (ctx , "LogReaderText" , 3 , d .Mach ,
func () (string , error ) {
return treeToText (d .logReader ), nil
})
return ret
}
func (d *Debugger ) hRemoveClient (id string ) {
delete (d .Clients , id )
d .hRemoveHistory (id )
delete (d .callLogLastSep , id )
delete (d .callLogCount , id )
delete (d .callLogFiles , id )
delete (d .callLogFilesLen , id )
}
func (d *Debugger ) callLogBootstrap (dir string ) error {
code := utils .Sp (`
package main
import (
am "github.com/pancsta/asyncmachine-go/pkg/machine"
// TODO edit these
"Mach"
"Mach/states"
)
var (
e *am.Event
active am.S
// TODO import schema
ss states.MachStatesDef
// TODO import handlers
h0 *MachHandlers
// h1 *MachHandlers2
)
` )
loc := filepath .Join (dir , "init.go" )
if _ , err := os .Stat (loc ); err != nil && os .IsNotExist (err ) {
return os .WriteFile (loc , []byte (code ), 0o644 )
} else if err != nil {
return err
}
return nil
}
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 .