// Package debugger provides a TUI debugger with multi-client support. Runnable // command can be found in tools/cmd/am-dbg.
package debugger // TODO // - ProcessFilterChange state // - DoUpdateLog state // - refac WalkUnsage to Walk via delayed writes, to fix races // - use the `hMethod` convention and impl Eval2Getter import ( _ amgraph amhelp am ssam ss amvis ) type S = am.S type cache struct { diagramId string diagramLvl int diagramDom *goquery.Document } type Debugger struct { *am.ExceptionHandler Mach *am.Machine Clients map[string]*Client // TODO make threadsafe Opts Opts LayoutRoot *cview.Panels // selected client // TODO atomic, drop eval C *Client App *cview.Application // printer for numbers TODO global P *message.Printer // TODO GC removed machines History []*types.MachAddress HistoryCursor int // UI is currently being drawn drawing atomic.Bool cache cache 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 statusBar *cview.TextView clientList *cview.List mainGrid *cview.Grid logRebuildEnd int lastScrolledTxTime time.Time // repaintScheduled controls the UI paint debounce repaintScheduled atomic.Bool // repaintPending indicates a skipped repaint repaintPending atomic.Bool genGraphsLast time.Time graph *amgraph.Graph // update client list scheduled updateCLScheduled atomic.Bool buildCLScheduled atomic.Bool lastKeystroke tcell.Key lastKeystrokeTime time.Time matrix *cview.Table focusManager *cview.FocusManager exportDialog *cview.Modal contentPanels *cview.Panels toolbars [3]*cview.Table schemaLogGrid *cview.Grid treeMatrixGrid *cview.Grid lastSelectedState string // TODO should be after a redraw, not before // redrawCallback is auto-disposed in draw() redrawCallback func() heartbeatT *time.Ticker logReader *cview.TreeView helpDialogRight *cview.TextView addressBar *cview.Table tagsBar *cview.TextView clip clipper.Clipboard // toolbarItems is a list of row of toolbars items toolbarItems [][]toolbarItem clientListFile *os.File txListFile *os.File msgsDelayed []*telemetry.DbgMsgTx msgsDelayedConns []string currTxBar *cview.Flex nextTxBar *cview.Flex mainGridCols []int readerExpanded map[string]bool treeGroups *cview.DropDown treeLayout *cview.Flex // list of states to show, bypassing other ones from the schema schemaTreeStates am.S lastSelectedGroup string // number of appended log msgs without a rebuild logAppends int logRenderedClient string lastResize uint64 logLastResize uint64 } // New creates a new debugger instance and optionally import a data file. func ( context.Context, Opts) (*Debugger, error) { var error // init the debugger := &Debugger{ Clients: make(map[string]*Client), readerExpanded: make(map[string]bool), } .Opts = // default opts if .Opts.Filters == nil { .Opts.Filters = &OptsFilters{ LogLevel: am.LogChanges, } } if .Opts.MaxMemMb == 0 { .Opts.MaxMemMb = maxMemMb } if .Opts.Log2Ttl == 0 { .Opts.Log2Ttl = time.Hour } gob.Register(server.Exportable{}) gob.Register(am.Relation(0)) := utils.RandId(0) if .Id != "" { = .Id } , := am.NewCommon(, "d-"+, ss.States, ss.Names, , nil, &am.Opts{ DontLogId: true, Tags: []string{"am-dbg"}, }) if != nil { return nil, } .Mach = // mach.AddBreakpoint1(ss.BuildingLog, "", false) // mach.AddBreakpoint1(ss.BuildingLog, "", true) // TODO update to schemas .SetGroupsString(map[string]am.S{ "Dialog": ss.GroupDialog, "Playing": ss.GroupPlaying, "Focused": ss.GroupFocused, "Views": ss.GroupViews, "SwitchedClientTx": ss.GroupSwitchedClientTx, "Filters": ss.GroupFilters, "Debug": ss.GroupDebug, }, []string{ "Dialog", "Playing", "Focused", "Views", "SwitchedClientTx", "Filters", "Debug", }) if .Opts.Version == "" { .Opts.Version = "(devel)" } // logging := .SemLogger() if .DbgLogger != nil { .SetSimple(.DbgLogger.Printf, .DbgLogLevel) } else { .SetSimple(log.Printf, .DbgLogLevel) } .SetArgsMapper(am.NewArgsMapper([]string{ // TODO extract "Client.id", "conn_id", "cursorTx1", "amount", "Client.id", "state", "fwd", "filter", "ToolName", "uri", "addr", "url", "err", "_am_err", }, 20)) .graph, = amgraph.New(.Mach) if != nil { .AddErr(fmt.Errorf("graph init: %w", ), nil) } // import data TODO state if .ImportData != "" { fmt.Printf("Importing data from %s\nPlease wait...\n", .ImportData) := time.Now() .Log("Importing data from %s", .ImportData) .hImportData(.ImportData) .Log("Imported data in %s", time.Since()) } // clipboard if .Opts.EnableClipboard { , := clipper.GetClipboard(clipper.Clipboards...) if != nil { .AddErr(fmt.Errorf("clipboard init: %w", ), nil) } .clip = } // ensure output directory exists if .Opts.OutputDir != "" && .Opts.OutputDir != "." { if := os.MkdirAll(.Opts.OutputDir, 0o755); != nil { .AddErr(fmt.Errorf("create output dir: %w", ), nil) } } // client list file if .Opts.OutputClients { := path.Join(.Opts.OutputDir, "am-dbg-clients.txt") , := os.Create() if != nil { .AddErr(fmt.Errorf("client list file open: %w", ), nil) } .clientListFile = } // tx file if .Opts.OutputTx { := path.Join(.Opts.OutputDir, "am-dbg-tx.md") , := os.Create() if != nil { .AddErr(fmt.Errorf("tx list file open: %w", ), nil) } .txListFile = } return , nil } // ///// ///// ///// // ///// PUB // ///// ///// ///// // hGetMachAddress returns the address of the currently visible view (mach, tx). func ( *Debugger) () *types.MachAddress { := .C if == nil { return nil } := &types.MachAddress{ MachId: .Id, MachTime: .MTimeSum, } if .CursorTx1 > 0 { .TxId = .MsgTxs[.CursorTx1-1].ID } if .CursorStep1 > 0 { .Step = .CursorStep1 } return } // hGoToMachAddress tries to render a view of the provided address (mach, tx). // Blocks. TODO state: GoToMachAddr, MachAddr func ( *Debugger) ( *types.MachAddress, bool, ) bool { // TODO should be an async state if .MachId == "" { return false } var <-chan struct{} := .Mach // select the target mach, if not selected if .C == nil || .C.Id != .MachId { // TODO ctx // TODO extract as amhelp.WhenNextActive if .Is1(ss.ClientSelected) { = .WhenTicks(ss.ClientSelected, 2, nil) } else { = .When1(ss.ClientSelected, nil) } := .Add1(ss.SelectingClient, am.A{"Client.id": .MachId}) if == am.Canceled { return false } } else { // TODO ctx = .When1(ss.ClientSelected, nil) } // TODO DONT BLOCK <- if .TxId != "" { // TODO typed args := am.A{"Client.txId": .TxId} if .Step != 0 { ["cursorStep1"] = .Step .Add1(ss.ScrollToStep, ) } .Add1(ss.ScrollToTx, ) } else if .MachTime != 0 { := .C.Tx(.C.TxByMachTime(.MachTime)) .Add1(ss.ScrollToTx, am.A{"Client.txId": .ID}) } else if !.HumanTime.IsZero() { := .C.Tx(.C.LastTxTill(.HumanTime)) .Add1(ss.ScrollToTx, am.A{"Client.txId": .ID}) } if ! { .hPrependHistory() .hUpdateAddressBar() // TODO only if main panel visible .draw(.addressBar) } return true } // func (d *Debugger) hSetCursor1( // cursor int, cursorStep int, skipHistory bool, // ) { // if d.C.CursorTx1 == cursor { // return // } // // // TODO validate // d.C.CursorTx1 = cursor // // if d.HistoryCursor == 0 && !skipHistory { // // add current mach if needed // if len(d.History) > 0 && d.History[0].MachId != d.C.id { // d.hPrependHistory(d.hGetMachAddress()) // } // // keeping the current tx as history head // if tx := d.C.tx(d.C.CursorTx1 - 1); tx != nil { // // dup the current machine if tx differs // if len(d.History) > 1 && d.History[1].MachId == d.C.id && // d.History[1].TxId != tx.ID { // // d.hPrependHistory(d.History[0].Clone()) // } // if len(d.History) > 0 { // d.History[0].TxId = tx.ID // } // } // } // // // debug // // d.Opts.DbgLogger.Printf("HistoryCursor: %d\n", d.HistoryCursor) // // d.Opts.DbgLogger.Printf("History: %v\n", d.History) // // if cursor == 0 { // d.lastScrolledTxTime = time.Time{} // } else { // tx := d.hCurrentTx() // d.lastScrolledTxTime = *tx.Time // // // tx file // if d.Opts.OutputTx { // index := d.C.MsgStruct.StatesIndex // _, _ = d.txListFile.WriteAt([]byte(tx.TxString(index)), 0) // } // } // // // reset the step timeline // // TODO validate // d.C.CursorStep1 = cursorStep // d.Mach.Remove1(ss.TimelineStepsScrolled, nil) // } func ( *Debugger) ( string) { := make([]*types.MachAddress, 0) for , := range .History { if <= .HistoryCursor && .HistoryCursor > 0 { .HistoryCursor-- } if .MachId == { continue } = append(, ) } .History = } func ( *Debugger) ( *types.MachAddress) { .History = slices.Concat([]*types.MachAddress{}, .History) .hTrimHistory() } // hTrimHistory will trim the head to the current position, making it the newest // entry func ( *Debugger) () { // remove head if .HistoryCursor > 0 { := .HistoryCursor if >= len(.History) { = len(.History) } .History = .History[:] } // prepend .HistoryCursor = 0 if len(.History) > 100 { .History = .History[100:] } // debug // d.Opts.DbgLogger.Printf("HistoryCursor: %d\n", d.HistoryCursor) // d.Opts.DbgLogger.Printf("History: %v\n", d.History) } func ( *Debugger) ( string) *Client { if .Clients == nil { return nil } , := .Clients[] if ! { return nil } return } func ( *Debugger) ( , string, ) (*Client, *telemetry.DbgMsgTx) { := .hGetClient() if == nil { return nil, nil } := .TxIndex() if < 0 { return nil, nil } := .Tx() if == nil { return nil, nil } return , } // Client returns the current Client. Thread safe via Eval(). func ( *Debugger) () *Client { var *Client // SelectingClient locks d.C TODO amhelp.WaitForAll <-.Mach.WhenNot1(ss.SelectingClient, nil) // TODO eval to getter .Mach.Eval("Client", func() { = .C }, nil) // TODO confirm c != nil, return err return } // NextTx returns the next transition. Thread safe via Eval(). func ( *Debugger) () *telemetry.DbgMsgTx { var *telemetry.DbgMsgTx // SelectingClient locks d.C <-.Mach.WhenNot1(ss.SelectingClient, nil) // TODO eval to getter .Mach.Eval("NextTx", func() { = .hNextTx() }, nil) // TODO confirm tx != nil, return err return } func ( *Debugger) () *telemetry.DbgMsgTx { := .C if == nil { return nil } := .CursorTx1 >= len(.MsgTxs) if { return nil } return .MsgTxs[.CursorTx1] } // CurrentTx returns the current transition. Thread safe via Eval(). func ( *Debugger) () *telemetry.DbgMsgTx { var *telemetry.DbgMsgTx // SelectingClient locks d.C <-.Mach.WhenNot1(ss.SelectingClient, nil) // TODO eval to getter .Mach.Eval("CurrentTx", func() { = .hCurrentTx() }, nil) // TODO confirm tx != nil, return err return } func ( *Debugger) () *telemetry.DbgMsgTx { := .C if == nil { return nil } if .CursorTx1 == 0 || len(.MsgTxs) < .CursorTx1 { return nil } return .MsgTxs[.CursorTx1-1] } // PrevTx returns the previous transition. Thread safe via Eval(). func ( *Debugger) () *telemetry.DbgMsgTx { var *telemetry.DbgMsgTx // SelectingClient locks d.C <-.Mach.WhenNot1(ss.SelectingClient, nil) // TODO eval to getter .Mach.Eval("PrevTx", func() { = .hPrevTx() }, nil) // TODO confirm tx != nil, return err return } func ( *Debugger) () *telemetry.DbgMsgTx { := .C if == nil { return nil } if .CursorTx1 < 2 { return nil } return .MsgTxs[.CursorTx1-2] } func ( *Debugger) () int { // if only 1 client connected, select it (if SelectConnected == true) var int for , := range .Clients { if .Connected.Load() { ++ } } return } func ( *Debugger) () { // TODO switch to Disposed mixin .Mach.Dispose() <-.Mach.WhenDisposed() // logger := .Opts.DbgLogger if != nil { // check if the logger is writing to a file if , := .Writer().(*os.File); { .Close() } } } // TODO config param to New( func ( *Debugger) ( string, int, string, string, ) { .Mach.Add1(ss.Start, am.A{ "Client.id": , "cursorTx1": , // TODO rename to uiView "dbgView": , "group": , }) } // TODO state: SetOptsState func ( *Debugger) ( am.LogLevel) { .Mach.Eval("SetFilterLogLevel", func() { .Opts.Filters.LogLevel = // process the toolbarItem change .Mach.Add1(ss.ToolToggled, nil) .hUpdateSchemaLogGrid() .hRedrawFull(false) }, nil) } // TODO state: ImportingData, DataImported func ( *Debugger) ( string) { // TODO show error msg (for dump old formats) // support URLs var *bufio.Reader , := url.Parse() if == nil && .Host != "" { // download , := http.Get() if != nil { .Mach.AddErr(, nil) return } = bufio.NewReader(.Body) } else { // read from fs , := os.Open() if != nil { .Mach.AddErr(, nil) return } defer .Close() = bufio.NewReader() } // decompress brotli := brotli.NewReader() // decode gob := gob.NewDecoder() var []*server.Exportable = .Decode(&) if != nil { .Mach.AddErr(fmt.Errorf("import failed %w", ), nil) return } // parse the data for , := range { := .MsgStruct.ID := amhelp.SchemaHash((*).MsgStruct.States) .Clients[] = newClient(, , , ) if .graph != nil { := .graph.AddClient(.MsgStruct) if != nil { .Mach.AddErr(fmt.Errorf("import failed %w", ), nil) return } } .Mach.Add1(ss.InitClient, am.A{"id": }) for := range .MsgTxs { .hParseMsg(.Clients[], ) } } // GC runtime.GC() } // ///// ///// ///// // ///// PRIV // ///// ///// ///// func ( *Debugger) () { := fmt.Sprintf // tx filters for , := range .toolbarItems { := .Mach.Is1(ss.Toolbar1Focused) switch { case 1: = .Mach.Is1(ss.Toolbar2Focused) case 2: = .Mach.Is1(ss.Toolbar3Focused) } for , := range { := "" , := .toolbars[].GetSelection() // checked := cview.Escape if .active != nil && .active() { if .activeLabel != nil { += (" [::b]%s[::-]", ("["+.activeLabel()+"]")) } else { += (" [::b]%s[::-]", ("[X]")) } // button - dedicated icon } else if .active == nil && .icon != "" { += (" [gray]%s[-]%s[gray]%s[-]", ("["), .icon, ("]")) // unchecked } else if .active == nil { += (" [grey][ ][-]") // button - default icon } else { += (" [ ]") } // focused if != -1 && .toolbarItems[][].id == ToolName(.id) && { += "[white]" + .label } else if ! { += ("[%s]%s", colorHighlight2, .label) } else { += ("%s", .label) } := .toolbars[].GetCell(0, ) .SetText() .SetTextColor(tcell.ColorWhite) .toolbars[].SetCell(0, , ) } } } func ( *Debugger) () { := "" := false := "" := "" if .C != nil { = .C.Id if .C.CursorTx1 > 0 { // TODO conflict with GC? = .C.MsgTxs[.C.CursorTx1-1].ID } if .C.CursorStep1 > 0 { // TODO conflict with GC? = strconv.Itoa(.C.CursorStep1) } = .C.Connected.Load() } // copy := .addressBar.GetCell(0, 6) .SetBackgroundColor(tcell.ColorLightGray) .SetTextColor(tcell.ColorBlack) if == "" { .SetSelectable(false) .SetBackgroundColor(tcell.ColorDefault) } else { .SetSelectable(true) } := .addressBar.GetCell(0, 8) .SetTextColor(tcell.ColorBlack) .SetBackgroundColor(tcell.ColorLightGray) // history := .addressBar.GetCell(0, 2) .SetBackgroundColor(tcell.ColorLightGray) .SetTextColor(tcell.ColorBlack) if .HistoryCursor > 0 { .SetSelectable(true) } else { .SetSelectable(false) .SetTextColor(tcell.ColorGray) .SetBackgroundColor(tcell.ColorDefault) } := .addressBar.GetCell(0, 0) .SetBackgroundColor(tcell.ColorLightGray) .SetTextColor(tcell.ColorBlack) if .HistoryCursor < len(.History)-1 { .SetSelectable(true) } else { .SetSelectable(false) .SetTextColor(tcell.ColorGray) .SetBackgroundColor(tcell.ColorDefault) } // detect clipboard if .clip == nil { .SetTextColor(tcell.ColorGrey) .SetSelectable(false) .SetBackgroundColor(tcell.ColorDefault) .SetTextColor(tcell.ColorGrey) .SetSelectable(false) .SetBackgroundColor(tcell.ColorDefault) } // address := "[grey]" if { = "[" + colorActive.String() + "]" } := .addressBar.GetCell(0, 4) if != "" && != "" { := "" if != "" { = "/" + } .SetText( + "mach://[-][::u]" + + "[::-][grey]/" + + ) } else if != "" { .SetText( + "mach://[-][::u]" + ) } else { .SetText("[grey]mach://[-]") } // tags := "" if != "" { if len(.C.MsgStruct.Tags) > 0 { += "[::b]#[::-]" + strings.Join(.C.MsgStruct.Tags, " [::b]#[::-]") } := .hGetParentTags(.C, nil) if len() > 0 { if != "" { += " ... " } += "[::b]#[::-]" + strings.Join(, " [::b]#[::-]") } } .tagsBar.SetText() } // hUpdateViews updates the contents of the currentl visible view. func ( *Debugger) ( bool) { switch .Mach.Switch(ss.GroupViews) { case ss.MatrixView: .hUpdateMatrix() .contentPanels.HidePanel("tree-log") .contentPanels.HidePanel("tree-matrix") .contentPanels.ShowPanel("matrix") case ss.TreeMatrixView: .hUpdateMatrix() .hUpdateSchemaTree() .contentPanels.HidePanel("matrix") .contentPanels.HidePanel("tree-log") .contentPanels.ShowPanel("tree-matrix") case ss.TreeLogView: fallthrough default: .hUpdateSchemaTree() if { .Mach.Add1(ss.UpdateLogScheduled, nil) } else { .Mach.Add1(ss.UpdateLogScheduled, nil) } .contentPanels.HidePanel("matrix") .contentPanels.HidePanel("tree-matrix") .contentPanels.ShowPanel("tree-log") } } // TODO remove? // hMemorizeTxTime will memorize the current tx time // func (d *Debugger) hMemorizeTxTime(c *Client) { // if c.CursorTx1 > 0 && c.CursorTx1 <= len(c.MsgTxs) { // d.lastScrolledTxTime = *c.MsgTxs[c.CursorTx1-1].Time // } // } func ( *Debugger) ( *Client, int) { // TODO handle panics from wrongly indexed msgs // defer d.Mach.PanicToErr(nil) // TODO verify connId := .MsgTxs[] var uint64 for , := range .Clocks { += } := .MsgStruct.StatesIndex := &telemetry.DbgMsgTx{} := &types.MsgTxParsed{} if len(.MsgTxs) > 1 && > 0 { = .MsgTxs[-1] = .MsgTxsParsed[-1] } // cast to Transition := &am.Transition{ TimeBefore: .Clocks, TimeAfter: .Clocks, Steps: .Steps, } // err if TimeAfter < TimeBefore, fake the rest := .TimeAfter.Sum(nil) := .TimeBefore.Sum(nil) if < { .Mach.AddErr(fmt.Errorf("time after < time before"), nil) .MTimeSum = .MsgTxsParsed = append(.MsgTxsParsed, &types.MsgTxParsed{TimeSum: }) .LogMsgs = append(.LogMsgs, make([]*am.LogEntry, 0)) return } , , := amhelp.GetTransitionStates(, ) := &types.MsgTxParsed{ TimeSum: , // TODO use in tx info bars TimeDiff: - .TimeSum, StatesAdded: .StatesToIndexes(), StatesRemoved: .StatesToIndexes(), StatesTouched: .StatesToIndexes(), } // optimize space if len(.CalledStates) > 0 { .CalledStatesIdxs = amhelp.StatesToIndexes(, .CalledStates) .MachineID = "" .CalledStates = nil } // TODO refac when dbg@v2 lands for , := range .Steps { if .FromState != "" || .ToState != "" { .FromStateIdx = slices.Index(, .FromState) .ToStateIdx = slices.Index(, .ToState) .FromState = "" .ToState = "" } // back compat if .Data != nil { .RelType, _ = .Data.(am.Relation) } } // errors var bool for , := range { if strings.HasPrefix(, am.PrefixErr) && .Is1(, ) { = true break } } if || .Is1(, am.StateException) { // prepend to errors .Errors = append([]int{}, .Errors...) } // store the parsed msg .MsgTxsParsed = append(.MsgTxsParsed, ) .MTimeSum = // logs and graph .hParseMsgLog(, , ) if .graph != nil { .graph.ParseMsg(.Id, ) } // rebuild the log to trim the head (unless importing) if .Mach.Is1(ss.Start) { .Mach.Add1(ss.BuildingLog, nil) } // DEBUG .CalledStates = amhelp.IndexesToStates(, .CalledStatesIdxs) } // hIsTxSkipped checks if the tx at the given index is skipped by toolbarItems // idx is 0-based func ( *Debugger) ( *Client, int) bool { if !.filtersActive() { return false } return slices.Index(.MsgTxsFiltered, ) == -1 } // hFilterTxCursor1 fixes the current cursor according to toolbarItems // by skipping filtered out txs. If none found, returns the current cursor. func ( *Debugger) ( *Client, int, bool) int { if !.filtersActive() { return } // skip filtered out txs for { if < 1 { return 0 } else if > len(.MsgTxs) { // not found if !.hIsTxSkipped(, .CursorTx1-1) { return .CursorTx1 } else { return 0 } } if .hIsTxSkipped(, -1) { if { -- } else { ++ } } else { break } } return } // TODO highlight selected state names, extract common logic func ( *Debugger) () { .currTxBarLeft.Clear() .currTxBarRight.Clear() .nextTxBarLeft.Clear() .nextTxBarRight.Clear() if .Mach.Not(am.S{ss.SelectingClient, ss.ClientSelected}) { .currTxBarLeft.SetText("Listening for connections on " + .Opts.AddrRpc) return } := .C := .hCurrentTx() if == nil { // c is nil when switching clients if == nil || len(.MsgTxs) == 0 { .currTxBarLeft.SetText("No transitions yet...") } else { .currTxBarLeft.SetText("Initial machine schema") } } else { var string switch .Mach.Switch(ss.GroupPlaying) { case ss.Playing: = formatTxBarTitle("Playing") case ss.TailMode: += formatTxBarTitle("Tail") + " " default: = formatTxBarTitle("Paused") + " " } , := .hGetTxInfo(.CursorTx1, , .MsgTxsParsed[.CursorTx1-1], ) .currTxBarLeft.SetText() .currTxBarRight.SetText() } := .hNextTx() if != nil && != nil { := "Next " , := .hGetTxInfo(.CursorTx1+1, , .MsgTxsParsed[.CursorTx1], ) .nextTxBarLeft.SetText() .nextTxBarRight.SetText() } } func ( *Debugger) () { // check for a ready client := .C if == nil { return } := len(.MsgTxs) := .hNextTx() .timelineSteps.SetTitleColor(cview.Styles.PrimaryTextColor) .timelineSteps.SetBorderColor(cview.Styles.PrimaryTextColor) .timelineSteps.SetFilledColor(cview.Styles.PrimaryTextColor) // grey rejected bars if != nil && !.Accepted { .timelineSteps.SetFilledColor(tcell.ColorGray) } // mark the last step of a canceled tx in red if != nil && .CursorStep1 == len(.Steps) && !.Accepted { .timelineSteps.SetFilledColor(tcell.ColorRed) } := 0 := .CursorTx1 >= if ! { = len(.MsgTxs[.CursorTx1].Steps) } // progressbar cant be max==0 .timelineTxs.SetMax(max(, 1)) // progress <= max .timelineTxs.SetProgress(.CursorTx1) // title var string if .filtersActive() { := slices.Index(.MsgTxsFiltered, .CursorTx1-1) + 1 if .CursorTx1 == 0 { = 0 } = .P.Sprintf(" Transition %d / %d [%s]%d / %d[-] ", , len(.MsgTxsFiltered), colorHighlight2, .CursorTx1, ) } else { = .P.Sprintf(" Transition %d / %d ", .CursorTx1, ) } .timelineTxs.SetTitle() .timelineTxs.SetEmptyRune(' ') // progressbar cant be max==0 .timelineSteps.SetMax(max(, 1)) // progress <= max .timelineSteps.SetProgress(.CursorStep1) .timelineSteps.SetTitle(fmt.Sprintf( " Next mutation step %d / %d ", .CursorStep1, )) .timelineSteps.SetEmptyRune(' ') } func ( *Debugger) () { := colorInactive if .Mach.IsErr() { = tcell.ColorRed } for , := range .focusable { .SetBorderColorFocused() } } // TODO state: ExportingData, DataExported // TODO remove log. func ( *Debugger) ( string, bool) { // validate the input if == "" { log.Printf("Error: export failed no filename") return } if len(.Clients) == 0 { log.Printf("Error: export failed no clients") return } // create file := path.Join(.Opts.OutputDir, +".gob.br") , := os.Create() if != nil { log.Printf("Error: export failed %s", ) return } defer .Close() // prepare the format := *.C.Tx(max(0, .C.CursorTx1-1)).Time := make([]*server.Exportable, len(.Clients)) := 0 for , := range .Clients { [] = .Exportable // snapshot limits to a single tx if { [].MsgTxs = []*telemetry.DbgMsgTx{.Tx(.LastTxTill())} } ++ } // create a new brotli writer := brotli.NewWriter() defer .Close() // encode := gob.NewEncoder() = .Encode() if != nil { log.Printf("Error: export failed %s", ) } } func ( *Debugger) ( int, *telemetry.DbgMsgTx, *types.MsgTxParsed, string, ) (string, string) { := := " " if == nil { return , } // left side var *telemetry.DbgMsgTx if > 1 { = .C.MsgTxs[-2] } // TODO limit state names to a group when [2]group := .CalledStateNames(.C.MsgStruct.StatesIndex) += .P.Sprintf(" | tx: %d", ) if .TimeDiff == 0 { += " | Time: [gray] 0[-]" } else { += .P.Sprintf(" | Time: +%d", .TimeDiff) } += " |" := "" if len() == 1 && .C.MsgStruct.States[[0]].Multi { += " multi" } if !.Accepted { += "[grey]" } += fmt.Sprintf(" %s%s: [::b]%s[::-]", .Type, , strings.Join(, ", ")) if !.Accepted { += "[-]" } // right side if .IsAuto { += "auto | " } if .IsCheck { += "check | " } if .IsQueued { += "[grey]queued[-] | " } else if !.Accepted { += "[grey]canceled[-] | " } // format time TODO against the prev filter-matched tx := .Time.Format(timeFormat) if != nil { := .Time.Format(timeFormat) if := findFirstDiff(, ); != -1 { = [:] + "[white]" + [:+1] + "[grey]" + [+1:] } } += fmt.Sprintf("add: %d | rm: %d | touch: %3s | [grey]%s", len(.StatesAdded), len(.StatesRemoved), strconv.Itoa(len(.StatesTouched)), , ) return , } func ( *Debugger) () bool { if len(.Clients) == 0 { return false } var []*Client for , := range .Clients { if !.Connected.Load() { = append(, ) } } // if all disconnected, clean up if len() == len(.Clients) { for , := range .Clients { // TODO cant be scheduled, as the client can connect in the meantime // d.Add1(ss.RemoveClient, am.A{"Client.id": c.id}) delete(.Clients, .Id) .hRemoveHistory(.Id) } if .graph != nil { .graph.Clear() } return true } return false } func ( *Debugger) () { if !.Mach.Any1(ss.MatrixView, ss.TreeMatrixView) { return } if .Mach.Is1(ss.MatrixRain) { .hUpdateMatrixRain() } else { .hUpdateMatrixRelations() } } func ( *Debugger) () { // TODO optimize: re-use existing cells or gen txt // TODO switch to rel matrix from helpers .matrix.Clear() .matrix.SetTitle(" Matrix ") := .C if == nil || .C.CursorTx1 == 0 { return } := .MsgStruct.StatesIndex if .SelectedGroup != "" { = .MsgSchemaParsed.Groups[.SelectedGroup] } var *telemetry.DbgMsgTx var *telemetry.DbgMsgTx if .CursorStep1 == 0 { = .hCurrentTx() = .hPrevTx() } else { = .hNextTx() = .hCurrentTx() } := .Steps := .CalledStateNames(.MsgStruct.StatesIndex) // show the current tx summary on step 0, and partial if cursor > 0 if .CursorStep1 > 0 { = [:.CursorStep1] } := -1 // TODO use pkg/x/helpers // called states var []int for , := range { := "0" if slices.Contains(, ) { = "1" = append(, ) } .matrix.SetCellSimple(0, , matrixCellVal()) // mark called states if slices.Contains(, ) { .matrix.GetCell(0, ).SetAttributes(tcell.AttrBold | tcell.AttrUnderline) } // mark selected state if .C.SelectedState == { .matrix.GetCell(0, ).SetBackgroundColor(colorHighlight3) = } } matrixEmptyRow(, 1, len(), ) // ticks := 0 for , := range { var uint64 if != nil { = .Clock(, ) } := .Clock(, ) := - += int() .matrix.SetCellSimple(2, , matrixCellVal(strconv.Itoa(int()))) := .matrix.GetCell(2, ) if == 0 { .SetTextColor(tcell.ColorGrey) } // mark called states if slices.Contains(, ) { .SetAttributes( tcell.AttrBold | tcell.AttrUnderline) } // mark selected state if .C.SelectedState == { .SetBackgroundColor(colorHighlight3) } } matrixEmptyRow(, 3, len(), ) // steps for , := range { for , := range { := 0 for , := range { // TODO style just the cells if .GetFromState(.MsgStruct.StatesIndex) == && ((.ToStateIdx == -1 && == ) || .GetToState(.MsgStruct.StatesIndex) == ) { += int(.Type) } := strconv.Itoa() = matrixCellVal() .matrix.SetCellSimple(+4, , ) := .matrix.GetCell(+4, ) // mark selected state if .C.SelectedState == || .C.SelectedState == { .SetBackgroundColor(colorHighlight3) } if == 0 { .SetTextColor(tcell.ColorGrey) continue } // mark called states if slices.Contains(, ) || slices.Contains(, ) { .SetAttributes(tcell.AttrBold | tcell.AttrUnderline) } else { .SetAttributes(tcell.AttrBold) } } } } := " Matrix:" + strconv.Itoa() + " " if .CursorTx1 > 0 { := strconv.Itoa(int(.MsgTxsParsed[.CursorTx1-1].TimeSum)) += "Time:t" + + " " } .matrix.SetTitle() } func ( *Debugger) () { if .Mach.Not1(ss.MatrixRain) { return } // TODO optimize: re-use existing cells? .matrix.Clear() .matrix.SetTitle(" Rain ") := .C if == nil { return } := -1 .matrix.SetSelectionChangedFunc(func(, int) { .Mach.Add1(ss.MatrixRainSelected, am.A{ // TODO typed args "row": , "column": , "currTxRow": , }) }) .matrix.SetSelectable(true, true) := .MsgStruct.StatesIndex if := .SelectedGroup; != "" { = .MsgSchemaParsed.Groups[] } := .hCurrentTx() := .hPrevTx() , , , := .matrix.GetInnerRect() -= 1 // collect tx to show, starting from the end (timeline 1-based index) // TODO renders rows 1 too many := []int{} := / 2 if .Mach.Is1(ss.TailMode) { = 0 } // TODO collect rows-amount before and after (always) and display, then fill // the missing rows from previously collected := .FilterIndexByCursor1(.CursorTx1) var int // ahead := func( int, int) bool { return < len(.MsgTxsFiltered) && len() <= } for := ; (, ); ++ { = append(, .MsgTxsFiltered[]) = } // behind := func( int) bool { return >= 0 && < len(.MsgTxsFiltered) && len() <= } for := - 1; (); -- { = slices.Concat([]int{.MsgTxsFiltered[]}, ) } // ahead again for := + 1; (, ); ++ { = append(, .MsgTxsFiltered[]) } for , := range { := "" = + 1 if == .CursorTx1 { // TODO keep idx using cell.SetReference(...) for the 1st cell in each row = } := .MsgTxs[-1] := .MsgTxsParsed[-1] := .CalledStateNames(.MsgStruct.StatesIndex) for , := range { := "." := strings.HasPrefix(, "Err") if .Is1(, ) { = "1" if slices.Contains(.StatesTouched, ) { = "2" } } else if slices.Contains(.StatesRemoved, ) { = "|" } else if !.Accepted && slices.Contains(, []) { // called but canceled = "c" } else if slices.Contains(.StatesTouched, ) { = "*" } += // init table .matrix.SetCellSimple(, , ) := .matrix.GetCell(, ) .SetSelectable(true) // gray out some if !.Accepted || == "." || == "|" || == "c" || == "*" { .SetTextColor(colorHighlight) } // mark called states if slices.Contains(, ) { .SetAttributes(tcell.AttrUnderline) } if == .CursorTx1 { // current tx .SetBackgroundColor(colorHighlight3) } else if .C.SelectedState == { // mark selected state .SetBackgroundColor(colorHighlight3) } if ( || == am.StateException) && .Is1(, ) { // mark exceptions if .Accepted { .SetBackgroundColor(tcell.ColorIndianRed) } else { .SetBackgroundColor(colorHighlight3) } } } // timestamp := .Time.Format(timeFormat) := // highlight first diff number since prev timestamp if > 1 { := .MsgTxs[-2].Time.Format(timeFormat) if := findFirstDiff(, ); != -1 { = [:] + "[white]" + [:+1] + "[gray]" + [+1:] } } // tail cell .matrix.SetCellSimple(, len(), fmt.Sprintf( " [gray]%d | %s[-]", , )) := .matrix.GetCell(, len()) // current tx if == .CursorTx1 { .SetBackgroundColor(colorHighlight3) } } := 0 if .CursorTx1 > 0 { for , := range { var uint64 if != nil { = .Clock(, ) } := .Clock(, ) := - += int() } } := " Matrix:" + strconv.Itoa() + " " if .CursorTx1 > 0 { := strconv.Itoa(int(.MsgTxsParsed[.CursorTx1-1].TimeSum)) += "Time:t" + + " " } .matrix.SetTitle() if .Mach.Is1(ss.TailMode) { // TODO restore column scroll .matrix.ScrollToEnd() } } func ( *Debugger) () { := .C if == nil { .statusBar.SetText("") return } := "" if .CursorStep1 > 0 { := .MsgTxs[.CursorTx1] := .Steps[.CursorStep1-1] = .StringFromIndex(.MsgStruct.StatesIndex) } // markdown to cview := 0 for strings.Contains(, "**") { := "[::b]" if %2 == 1 { = "[::-]" } ++ = strings.Replace(, "**", , 1) } .statusBar.SetText() } func ( *Debugger) () int { if .C == nil { return -1 } := 0 for , := range .clientList.GetItems() { := .GetReference().(*sidebarRef) if .name == .C.Id { return } ++ } return -1 } // hFilterClientTxs filters client's txs according the selected // toolbarItems. Called by toolbarItem states, not directly. func ( *Debugger) () { if .C == nil || !.filtersActive() { return } .C.MsgTxsFiltered = nil for := range .C.MsgTxs { := .hFilterTx(.C, , .filtersFromStates()) if { .C.MsgTxsFiltered = append(.C.MsgTxsFiltered, ) } } } func ( *Debugger) () *OptsFilters { := .Mach.Is1 return &OptsFilters{ SkipCanceledTx: (ss.FilterCanceledTx), SkipAutoTx: (ss.FilterAutoTx), SkipAutoCanceledTx: (ss.FilterAutoCanceledTx), SkipEmptyTx: (ss.FilterEmptyTx), SkipHealthTx: (ss.FilterHealth), SkipQueuedTx: (ss.FilterQueuedTx), SkipOutGroup: (ss.FilterOutGroup), SkipChecks: (ss.FilterChecks), } } // filtersActive checks if any filters are active. func ( *Debugger) () bool { return .Mach.Any1(ss.GroupFilters...) } // hFilterTx returns true when a TX passes selected toolbarItems. func ( *Debugger) ( *Client, int, *OptsFilters) bool { := .MsgTxs[] := .MsgTxsParsed[] := .CalledStateNames(.MsgStruct.StatesIndex) := .SelectedGroup := // basic filters if .SkipAutoTx && .IsAuto { return false } else if .SkipAutoCanceledTx && .IsAuto && !.Accepted { return false } if .SkipCanceledTx && !.Accepted { return false } if .SkipQueuedTx && .IsQueued { return false } if .SkipChecks && .IsCheck { return false } // filter out txs without called from the group (if any) if .SkipOutGroup && != "" { := .MsgSchemaParsed.Groups[] if len(am.StatesShared(, )) == 0 { return false } } // skip empty (except queued and canceled) if .SkipEmptyTx && .TimeDiff == 0 && !.IsQueued && .Accepted { return false } // healthcheck if .SkipHealthTx { := S{ssam.BasicStates.Healthcheck, ssam.BasicStates.Heartbeat} if len() == 1 && slices.Contains(, [0]) { return false } } return true } func ( *Debugger) ( *am.Event, time.Time, bool, ) bool { if .C == nil { return false } := .C.LastTxTill() if == -1 { return false } if { = .hFilterTxCursor1(.C, , true) } .hSetCursor1(, am.A{ "cursor1": , "filterBack": true, }) return true } func ( *Debugger) ( *Client, []string) []string { , := .Clients[.MsgStruct.Parent] if ! { return } = slices.Concat(, .MsgStruct.Tags) return .(, ) } func ( *Debugger) ( *amgraph.Graph, string, , int, , string, S, ) []*amvis.Renderer { var []*amvis.Renderer // render single (current one) := amvis.NewRenderer(, .Mach.Log) amvis.PresetSingle() // TODO enum switch { default: return // single (simple) case 1: .RenderNestSubmachines = false .RenderStart = false .RenderInherited = false .RenderPipes = false .RenderHalfPipes = false .RenderHalfConns = false .RenderHalfHierarchy = false .RenderParentRel = false // single (detailed) case 2: .RenderNestSubmachines = false .RenderStart = true .RenderInherited = true .RenderPipes = false .RenderHalfPipes = false .RenderHalfConns = false .RenderHalfHierarchy = false // single (external) case 3: .RenderNestSubmachines = true .RenderStart = true .RenderInherited = true .RenderPipes = true } .Mach.Log("rendering graphs lvl %d", ) // machine diagram .RenderMachs = []string{} .OutputFilename = path.Join(, ) if len() > 0 { .RenderAllowlist = } = append(, ) // TODO render by prefixes // } else { // for _, p := range strings.Split(d.Opts.Graph, ",") { // // // vis // vis := amvis.NewRenderer(d.Mach, shot) // if p == "1" || p == "true" { // // render single (current one) // amvis.PresetSingle(vis) // vis.RenderMachs = []string{p} // vis.RenderNestSubmachines = true // vis.RenderStart = true // } else { // // render by prefix // prefix := regexp.MustCompile("^" + p) // amvis.PresetNeighbourhood(vis) // vis.RenderMachsRe = []*regexp.Regexp{prefix} // vis.RenderDistance = 1 // vis.RenderDepth = 1 // } // // prefix with am-vis // vis.OutputFilename = path.Join(d.Opts.OutputDir, "am-vis-"+p) // // vizs = append(vizs, vis) // } // } // TODO render a mutation // map // TODO skip if there was no change in schemas // hash schemas and schema's hashes, then compare // map renderer, check cache := fmt.Sprintf("am-vis-map-%d", ) if , := os.Stat(); != nil { = amvis.NewRenderer(, .Mach.Log) amvis.PresetMap() .OutputFilename = path.Join(, ) } return append(, ) } func ( *Debugger) () { switch .Opts.Timelines { case 0: .Mach.Add(S{ss.TimelineTxHidden, ss.TimelineStepsHidden}, nil) case 1: .Mach.Add1(ss.TimelineStepsHidden, nil) .Mach.Remove1(ss.TimelineTxHidden, nil) case 2: .Mach.Remove(S{ss.TimelineStepsHidden, ss.TimelineTxHidden}, nil) } } func ( *Debugger) ( *am.Event, *amgraph.Graph, string, , int, string, string, S, ) { := .Mach.NewStateCtx(ss.DiagramsRendering) if .Err() != nil { return // expired } // mark rendering as in-progress .Mach.EvRemove1(, ss.DiagramsScheduled, nil) // create the visualizer := .initGraphGen(, , , , , , ) // TODO config := amhelp.Pool(2) for , := range { .Go(func() error { return .GenDiagrams() }) } := .Wait() if != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } // link .diagramLink(, ) if .Err() != nil { return // expired } // symlink am-vis-map.svg -> am-vis-map-CLIENTS.svg := path.Join(, "am-vis-map.svg") := fmt.Sprintf("am-vis-map-%d.svg", ) _ = os.Remove() = os.Symlink(, ) if .Err() != nil { return // expired } .Mach.EvAddErr(, , nil) // next .Mach.EvAdd1(, ss.DiagramsReady, nil) } func ( *Debugger) ( string, string) { // symlink am-vis.svg -> ID-LVL-HASH.svg := path.Join(, "am-vis.svg") := fmt.Sprintf("%s.svg", ) _ = os.Remove() := os.Symlink(, ) .Mach.AddErr(, nil) } func ( *Debugger) ( *am.Event, string, *goquery.Document, *telemetry.DbgMsgTx, , string, ) { := path.Join(, +".svg") := .Mach.NewStateCtx(ss.DiagramsRendering) if .Err() != nil { return // expired } // mark rendering as in-progress .Mach.EvRemove1(, ss.DiagramsScheduled, nil) // update cache DOM := .C.MsgStruct.StatesIndex := amvis.Fragment{ MachId: , States: , Active: .ActiveStates(), } := amvis.UpdateCache(, , , &) if .Err() != nil { return // expired } if != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } // link .diagramLink(, ) if .Err() != nil { return // expired } // next .Mach.EvAdd1(, ss.DiagramsReady, nil) } func ( *Debugger) ( *am.Event, string, *telemetry.DbgMsgTx, int, , string, ) { defer .Mach.PanicToErrState(ss.ErrDiagrams, nil) := path.Join(, +".svg") := .Mach.NewStateCtx(ss.DiagramsRendering) if .Err() != nil { return // expired } // mark rendering as in-progress .Mach.EvRemove1(, ss.DiagramsScheduled, nil) // read cache from a file , := os.Open() if != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } , := goquery.NewDocumentFromReader() if != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } // update cache DOM // TODO support groups := .C.MsgStruct.StatesIndex := amvis.Fragment{ MachId: , States: , } if != nil { .Active = .ActiveStates() } = amvis.UpdateCache(, , , &) if .Err() != nil { return // expired } if != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } // link .diagramLink(, ) if .Err() != nil { return // expired } // next .Mach.EvAdd1(, ss.DiagramsReady, am.A{ // TODO typed args "Diagram.cache": , "Diagram.id": , "Diagram.lvl": , }) } func ( *Debugger) () tcell.Color { := cview.Styles.MoreContrastBackgroundColor if .Mach.IsErr() { = tcell.ColorRed } return }