// 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 arpc ssam ) type ( S = am.S A = types.A ) var ( Pass = am.Pass ss = states.DebuggerStates // printer for numbers TODO global P = message.NewPrinter(language.English) ) // TODO avoid globals var theme Theme type Debugger struct { *am.ExceptionHandler *ssam.DisposedHandlers Mach *am.Machine Clients map[string]*Client // graphHash is a hash of all schema hashes graphHash string LayoutRoot *cview.Panels // selected client C *Client App *cview.Application // TODO GC removed machines History []*types.MachAddress HistoryCursor int // TODO pass via state params types.Params // read-only params clone Params atomic.Pointer[types.Params] // UI is currently being drawn 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 controls the UI paint debounce repaintScheduled atomic.Bool // repaintPending indicates a skipped repaint repaintPending atomic.Bool graph *amgraph.Graph // update client list scheduled 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] // TODO should be after a redraw, not before // redrawCallback is auto-disposed in draw() redrawCallback func() heartbeatT *time.Ticker logReader *cview.TreeView helpDialogLeft *cview.TextView helpDialogRight *cview.TextView addressBar *cview.Table tagsBar *cview.TextView clip clipper.Clipboard // toolbarItems is a list of row of toolbars items toolbarItems [4][]toolbarItem clientListFile *os.File txFileMd *os.File msgsDelayed []*dbg.DbgMsgTx msgsDelayedConns []string currTxBar *cview.Flex nextTxBar *cview.Flex mainGridCols []int // reader tree root node names to expanded state logReaderExpanded map[string]bool logReaderScroll int // fallback for Y-based selection restore logReaderSelectedY int // semantic selection restore logReaderSelected string logReaderSelectedLevel int logReaderSelectedParent string treeGroups *cview.DropDown treeLayout *cview.Flex // list of states to show, bypassing other ones from the schema schemaTreeStates am.S selectedGroup atomic.Pointer[string] // number of appended log msgs without a rebuild logAppends int logRenderedClient string lastResize uint64 logLastResize uint64 sshSrv *ssh.Server logFile *os.File // TODO handle in LogBuiltState logFileMx sync.Mutex statusBarLeft *cview.TextView focusablePrims []cview.Primitive mouseFocusChanged bool Focused cview.Primitive // skip the next selection action 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 // length of the current file callLogFilesLen map[string]int64 // num of calls in the current file callLogCount map[string]int // count value of the last active separator callLogLastSep map[string]int // host to connect to, resolved from 0.0.0.0 listenHost string listenAddrRpc string listenAddrHttp string listenAddrSsh string // diagrams // machine diagram cache diagMachDom atomic.Pointer[goquery.Document] // machine diagram name (no dir, no extension) diagMachName atomic.Pointer[string] // state diagram cache diagStateDom atomic.Pointer[goquery.Document] // state diagram path (no extension) 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 } // TODO split to New and Init func ( context.Context, types.Params) (*Debugger, error) { var error // init the debugger := &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), } // pointer defs .diagSkipGroup.Store(new(string)) .diagMachName.Store(new(string)) .diagStatePath.Store(new(string)) .selectedState.Store(new(string)) .selectedGroup.Store(new(string)) .selectedSchemaHash.Store(new(string)) .selectedClient.Store(new(string)) // TODO params def := utils.RandId(0) if .Id != "" { = .Id } , := am.NewCommon(, "d-"+, states.DebuggerSchema, ss.Names(), , nil, &am.Opts{ DontLogId: true, Tags: []string{"am-dbg"}, }) if != nil { return nil, } .Mach = .SetGroups(states.DebuggerGroups, ss) // self debug if .DebugAddr != "" { _ = amhelp.MachDebug(, .DebugAddr, .LogLevel, false, amhelp.SemConfigEnv(true)) } if .Repl { // start a dedicated aRPC server for the REPL, create an addr file = arpc.MachRepl(, "", &arpc.ReplOpts{ AddrDir: .OutputDir, Args: types.ArgsRpc, }) .AddErr(, nil) } // mach.AddBreakpoint1(ss.AddressFocused, "", false) // mach.AddBreakpoint1(ss.AddressFocused, "", true) // mach.AddBreakpoint1(ss.Disposing, "", false) // mach.AddBreakpoint1(ss.Disposing, "", true) = .hSetParams() if != nil { return nil, } if .params.Version == "" { .params.Version = "(devel)" } // logging := .SemLogger() if .params.DbgLogger != nil { .SetSimple(.params.DbgLogger.Printf, .params.LogLevel) } else { .SetSimple(log.Printf, .params.LogLevel) } .SetArgsMapper(amhelp.LogArgsMapper) .graph, = amgraph.New(.Mach) if != nil { .AddErr(fmt.Errorf("graph init: %w", ), nil) } // import data TODO state if .params.ImportData != "" { .params.Print("Importing data from %s\nPlease wait...\n", .params.ImportData) := time.Now() .Log("Importing data from %s", .params.ImportData) .hImportData(.params.ImportData) if .Mach.IsErr() { .params.Print("ERROR: %s\n", .Mach.Err()) } else { .Log("Imported data in %s", time.Since()) } } .OnDispose(func( string, context.Context) { .Dispose() }) return , nil } func ( *Debugger) ( types.Params) error { := .Mach // validate if .LogLevel > am.LogEverything { .LogLevel = am.LogEverything } if .FilterLogLevel > am.LogEverything { .FilterLogLevel = am.LogEverything } .OutputDiagrams.Value = max(.OutputDiagrams.Value, .OutputDiagrams.Value) .ViewTimelines.Value = max(.ViewTimelines.Value, .ViewTimelines.Value) // rain adjusts the default view if .ViewRain && .StartupView == "tree-log" { .StartupView = "tree-matrix" } // compute addr := "" := "" := .ListenAddr if .ListenAddr != "-1" && .ListenAddr != "" { , , := net.SplitHostPort(.ListenAddr) .listenHost = // global listen if == "0.0.0.0" { , := utils.GetGlobalUnicastIP() if != nil { return } .listenHost = .Log("public host: %s", ) } if == nil { , := strconv.Atoi() = + ":" + strconv.Itoa(+1) = + ":" + strconv.Itoa(+2) .listenAddrRpc = .listenHost + ":" + strconv.Itoa() .listenAddrHttp = .listenHost + ":" + strconv.Itoa(+1) .listenAddrSsh = .listenHost + ":" + strconv.Itoa(+2) } } if !.UiSsh { = "" } if !.UiWeb { = "" } .AddrRpc = .AddrHttp = .AddrSsh = .UiSsh = .UiSsh && != "" .UiWeb = .UiWeb && != "" .TailMode = .TailMode && .MachUrl == "" .Version = utils.GetVersion() // default filters if .Filters == nil { .Filters = &types.Filters{ LogLevel: .FilterLogLevel, SkipOutGroup: .FilterGroup, SkipCanceledTx: .FilterCanceledTx, SkipAutoTx: .FilterAutoTx, SkipAutoCanceledTx: .FilterAutoCanceledTx, SkipEmptyTx: .FilterEmptyTx, SkipHealthTx: .FilterHealthTx, SkipQueuedTx: .FilterQueuedTx, SkipChecks: .FilterChecks, SkipRpcMach: .FilterRpcMachs, } } .statesFromFilters(.Filters) // other defaults if .Print == nil { .Print = func( string, ...any) { fmt.Printf(, ...) } } if .FilterDisconn { .Mach.Add1(ss.FilterDisconn, nil) } else { .Mach.Remove1(ss.FilterDisconn, nil) } // TODO avoid globals var error if .ViewTheme == "light" { theme, = mapToTheme(themeLight, false) } else { theme, = mapToTheme(themeDark, true) } if != nil { return } // apply theme to defaults theme.Apply() // clipboard if .EnableClipboard { , := clipper.GetClipboard(clipper.Clipboards...) if != nil { .AddErr(fmt.Errorf("clipboard init: %w", ), nil) } .clip = } // TODO dispose old files // ensure dirs exist = os.MkdirAll(path.Join(.OutputDir, "diagrams"), 0o755) if != nil { return } = os.MkdirAll(path.Join(.OutputDir, "call-log"), 0o755) if != nil { return } // client list file if .OutputClients { := path.Join(.OutputDir, "clients.txt") , := os.Create() if != nil { .AddErr(, nil) } .clientListFile = } // graph files if .OutputGraph { := path.Join(.OutputDir, "graph.md") .graphFileMd, = os.Create() if != nil { .AddErr(, nil) } = path.Join(.OutputDir, "graph.xml") .graphFileMgml, = os.Create() if != nil { .AddErr(, nil) } } // expand links .logReaderExpanded["__link_nodes"] = .ViewExpandLinks // SAVE .params = := .Params.Store(&) // log file if .OutputLog { .hInitLogFile() } // tx files if .OutputTx { if := .hInitTxFile(); != nil { return } } if .OutputDiagrams != types.ParamsOutputDiagramsNone { if := .hInitDiagFiles(); != nil { return } } return nil } func ( *Debugger) () error { // markdown := path.Join(.params.OutputDir, "tx.md") , := os.Create() if != nil { return } .txFileMd = return nil } func ( *Debugger) () error { if .txFileMd == nil { return nil } return .txFileMd.Close() } func ( *Debugger) () { := path.Join(.params.OutputDir, logFile) , := os.Create() if != nil { .Mach.AddErr(, nil) return } .logFile = } // TODO error func ( *Debugger) () { .logFileMx.Lock() defer .logFileMx.Unlock() .Mach.AddErr(.logFile.Close(), nil) .logFile = nil } func ( *Debugger) () { for , := range .callLogFiles { .Close() } .callLogFiles = make(map[string]*os.File) } func ( *Debugger) () error { := .params.OutputDir := path.Join(, "diagrams") var error // TODO CLI flag // D2 := path.Join(, "steps.d2") .diagStepsFileD2, = os.Create() if != nil { return nil } := path.Join(, "steps.d2.svg") .diagStepsFileD2Svg, = os.Create() if != nil { return } // mermaid := path.Join(, "steps.mermaid") .diagStepsFileMermaid, = os.Create() if != nil { return } := path.Join(, "steps.mermaid.txt") .diagStepsFileMermaidAscii, = os.Create() if != nil { return } return nil } func ( *Debugger) () { if .diagStepsFileD2Svg == nil { return } .diagStepsFileD2Svg.Close() .diagStepsFileD2.Close() .diagStepsFileMermaid.Close() .diagStepsFileMermaidAscii.Close() } // hSetCursor1 sets both the tx and steps cursors, 1-based. func ( *Debugger) ( *am.Event, *A) { := .Cursor1 := .CursorStep1 := .SkipHistory := .TrimHistory := .FilterBack := .C := .hCurrentTx() // ctx if .ctxCancelCursor != nil { .ctxCancelCursor() } .ctxCursor, .ctxCancelCursor = context.WithCancel(.Mach.Context()) := am.EvToCtx(.ctxCursor, ) // TODO optimize for no-change? .CursorTx1 = .hFilterTxCursor1(, , ) // reset the step timeline // TODO validate .CursorStep1 = if .HistoryCursor == 0 && ! { .hPrependHistory(.hGetMachAddress()) } else if { .hTrimHistory() } .hHandleTStepsScrolled() // debug // d.State.DbgLogger.Printf("HistoryCursor: %d\n", d.HistoryCursor) // d.State.DbgLogger.Printf("History: %v\n", d.History) .lastScrolledTxTime = time.Time{} if != nil { .Mach.EvAdd1(, ss.TxSelected, nil) := .hCurrentTx() if != nil { .lastScrolledTxTime = *.Time } // diagram steps if .params.OutputDiagrams != types.ParamsOutputDiagramsNone && != nil { .Mach.GoAfter(.ctxCursor, time.Second/2, func() { := .TxParsed(.CursorTx1 - 1) // TODO parsed msg nil if == nil { := fmt.Errorf("parsed tx missing for %s", .ID) .Mach.AddErrState(ss.ErrDiagrams, , nil) return } // render diagram .hDiagramsStepsRendering(, , ) // TODO update state diagram }) } } else { .Mach.EvRemove1(, ss.TxSelected, nil) } .Mach.EvRemove1(, ss.TimelineStepsScrolled, nil) // diagrams TODO merged mutation once partial negotiation lands .Mach.GoAfter(, time.Second, func() { if := .diagramsMachUpdating(); != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } }) .Mach.GoAfter(, time.Second, func() { if := .diagramsStateUpdating(); != nil { .Mach.EvAddErrState(, ss.ErrDiagrams, , nil) return } }) } func ( *Debugger) () { := "" for , := range .Clients { += amhelp.SchemaHash(.MsgStruct.States) } .graphHash = am.Hash(, 20) } // hGetMachAddress returns the address of the currently visible view (mach, tx). func ( *Debugger) () *types.MachAddress { := .C if == nil { return nil } := &types.MachAddress{ MachId: .Id, } // TODO getter if .CursorTx1 > 0 { := .MsgTxs[.CursorTx1-1] .TxId = .ID .MachTime = .TimeSum() // TODO queue tick } if .CursorStep1 > 0 { .Step = .CursorStep1 } // GET params .State = .SelectedState .Group = .SelectedGroup return } // GoToMachAddress 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 // TODO ctx := context.TODO() if .MachId == "" { return false } var <-chan struct{} := .Mach // select the target mach, if not selected if .C == nil || .C.Id != .MachId { // TODO extract as amhelp.WhenNextActive if .Is1(ss.ClientSelected) { // TODO next active in () = .WhenTicks(ss.ClientSelected, 2, ) } else { = .When1(ss.ClientSelected, ) } := .Add1(ss.SelectingClient, Pass(&A{ ClientId: .MachId, })) if == am.Canceled { return false } } else { = .When1(ss.ClientSelected, ) } // TODO timeout <- if .TxId != "" { := &A{ TxId: .TxId, CursorStep1: .Step, } .Add1(ss.ScrollToTx, Pass()) } else if .MachTime != 0 { := .C.Tx(.C.TxAtMachTime(.MachTime)) .Add1(ss.ScrollToTx, Pass(&A{ TxId: .ID, })) } else if !.HumanTime.IsZero() { := .C.Tx(.C.TxAtHTime(.HumanTime)) .Add1(ss.ScrollToTx, Pass(&A{ TxId: .ID, })) } else if .QueueTick != 0 { := .C.Tx(.C.TxAtQueueTick(.QueueTick)) .Add1(ss.ScrollToTx, Pass(&A{ TxId: .ID, })) } .hUpdateAddressBar() // GET params if .State != "" { .Mach.Add1(ss.StateNameSelected, Pass(&A{ State: .State, })) } if .Group != "" { // TODO fix group IDs, merge with SetGroupState .Mach.Eval("GoToMachAddress", func() { := "" for := range .C.MsgStruct.Groups { if types.NormalizeGroupName() == types.NormalizeGroupName(.Group) { = break } } .selectedGroup.Store(&) .C.SelectedGroup = .hBuildSchemaTree() .hUpdateSchemaTree() .hUpdateTreeGroups() .Mach.Add(am.S{ss.ToolToggled, ss.UpdateLogScheduled}, Pass(&A{ FilterTxs: true, LogRebuildEnd: len(.C.MsgTxs), })) }, nil) } else { .Mach.Add1(ss.SetGroup, Pass(&A{ Group: "all", })) } // TODO remove if .Step != 0 { .Add1(ss.ScrollToStep, Pass(&A{ CursorStep1: .Step, })) } 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.Params.DbgLogger.Printf("HistoryCursor: %d\n", d.HistoryCursor) // // d.Params.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.Params.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) { // add hist if URL changes (excl GET params) if len(.History) > 0 && .History[0].StringBase() == .StringBase() { return } .History = slices.Concat([]*types.MachAddress{}, .History) // dbg := make([]string, len(d.History)) // for i := range d.History { // dbg[i] = d.History[i].String() // } // dump.Println(dbg) .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.Params.DbgLogger.Printf("HistoryCursor: %d\n", d.HistoryCursor) // d.Params.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, *dbg.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, error) { // SelectingClient locks d.C TODO amhelp.WaitForAll <-.Mach.WhenNot1(ss.SelectingClient, nil) , := amhelp.EvalGetter(nil, "Client", 3, .Mach, func() (*Client, error) { return .C, nil }) if == nil { return nil, fmt.Errorf("no client selected") } return , nil } // NextTx returns the next transition. Thread safe via Eval(). func ( *Debugger) () *dbg.DbgMsgTx { // SelectingClient locks d.C <-.Mach.WhenNot1(ss.SelectingClient, nil) , := amhelp.EvalGetter(.Mach.Context(), "NextTx", 3, .Mach, func() (*dbg.DbgMsgTx, error) { return .hNextTx(), nil }) return } func ( *Debugger) () *dbg.DbgMsgTx { := .hNextTxIdx() if < 0 { return nil } return .C.MsgTxs[] } func ( *Debugger) () int { := .C if == nil { return -1 } return .hFilterTxCursor1(, .CursorTx1+1, false) - 1 } // CurrentTx returns the current transition. Thread safe via Eval(). func ( *Debugger) () *dbg.DbgMsgTx { var *dbg.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) () *dbg.DbgMsgTx { := .C if == nil { return nil } if .CursorTx1 == 0 || len(.MsgTxs) < .CursorTx1 { return nil } return .MsgTxs[.CursorTx1-1] } func ( *Debugger) () *types.MsgTxParsed { := .C if == nil { return nil } if .CursorTx1 == 0 || len(.MsgTxsParsed) < .CursorTx1 { return nil } return .C.MsgTxsParsed[.C.CursorTx1-1] } // PrevTx returns the previous transition. Thread safe via Eval(). func ( *Debugger) () *dbg.DbgMsgTx { <-.Mach.WhenNot1(ss.SelectingClient, nil) , := amhelp.EvalGetter(nil, "PrevTx", 3, .Mach, func() (*dbg.DbgMsgTx, error) { return .hPrevTx(), nil }) // TODO confirm tx != nil, return err return } func ( *Debugger) () *dbg.DbgMsgTx { := .hPrevTxIdx() if < 0 { return nil } return .C.MsgTxs[] } func ( *Debugger) () int { := .C if == nil { return -1 } return .hFilterTxCursor1(, .CursorTx1-1, true) - 1 } func ( *Debugger) () int { // if only 1 client connected, select it (if SelectConnected == true) var int for , := range .Clients { if .Connected.Load() { ++ } } return } func ( *Debugger) () *types.MachAddress { , := amhelp.EvalGetter(.Mach.Context(), "MachAddr", 3, .Mach, func() (*types.MachAddress, error) { if .C == nil { return nil, fmt.Errorf("no client selected") } := &types.MachAddress{ MachId: .C.Id, } if .C.CursorTx1 > 0 { := .C.MsgTxs[.C.CursorTx1-1] .TxId = .ID .MachTime = .C.MsgTxsParsed[.C.CursorTx1-1].TimeSum .HumanTime = *.Time .QueueTick = .QueueTick } if .C.CursorStep1 > 0 { .Step = .C.CursorStep1 } if .C.SelectedGroup != "" { .Group = .C.SelectedGroup } return , nil }) return } func ( *Debugger) () { // TODO switch to Disposed mixin // logger := .params.DbgLogger if != nil { // check if the logger is writing to a file if , := .Writer().(*os.File); { .Close() } } } func ( *Debugger) () { .Mach.Add1(ss.Start, nil) } // TODO state: SetOptsState func ( *Debugger) ( am.LogLevel) { .Mach.Eval("SetFilterLogLevel", func() { .params.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, Pass(&A{ Id: , })) for := range .MsgTxs { .hParseMsg(.Clients[], ) } } // update graph file amgraph.AddErrGraph(nil, .Mach, .hUpdateGraphFile(nil)) // 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) case 3: = .Mach.Is1(ss.Toolbar4Focused) } 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 != "" { += (" ["+theme.Grey+"]%s[-]%s["+theme.Grey+"]%s[-]", ("["), .icon, ("]")) // unchecked } else if .active == nil { += (" [" + theme.Grey + "][ ][-]") // button - default icon } else { += (" [ ]") } // focused if != -1 && .toolbarItems[][].id == .id && { += "[" + theme.White + "]" + .label } else if ! { += ("[%s]%s", theme.Grey, .label) } else { += ("%s", .label) } := .toolbars[].GetCell(0, ) .SetText() .SetTextColor(tcell.GetColor(theme.White)) .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, colCopy) .SetBackgroundColor(tcell.GetColor(theme.LightGrey)) .SetTextColor(tcell.GetColor(theme.BgPrimary)) if == "" { .SetSelectable(false) .SetBackgroundColor(tcell.ColorDefault) } else { .SetSelectable(true) } := .addressBar.GetCell(0, colPaste) .SetTextColor(tcell.GetColor(theme.BgPrimary)) .SetBackgroundColor(tcell.GetColor(theme.LightGrey)) // history fwd := .addressBar.GetCell(0, colNext) .SetBackgroundColor(tcell.GetColor(theme.LightGrey)) .SetTextColor(tcell.GetColor(theme.BgPrimary)) := .addressBar.GetCell(0, colNextMach) .SetBackgroundColor(tcell.GetColor(theme.LightGrey)) .SetTextColor(tcell.GetColor(theme.BgPrimary)) .SetSelectable(true) if .HistoryCursor > 0 { .SetSelectable(true) } else { .SetSelectable(false) .SetTextColor(tcell.GetColor(theme.Grey)) .SetBackgroundColor(tcell.ColorDefault) } // scan until machId changes := false for := .HistoryCursor; > 0; -- { if .History[].MachId != .History[-1].MachId { = true break } } if ! { .SetSelectable(false) .SetTextColor(tcell.GetColor(theme.Grey)) .SetBackgroundColor(tcell.ColorDefault) } // history back := .addressBar.GetCell(0, colPrev) .SetBackgroundColor(tcell.GetColor(theme.LightGrey)) .SetTextColor(tcell.GetColor(theme.BgPrimary)) := .addressBar.GetCell(0, colPrevMach) .SetBackgroundColor(tcell.GetColor(theme.LightGrey)) .SetTextColor(tcell.GetColor(theme.BgPrimary)) .SetSelectable(true) if .HistoryCursor < len(.History)-1 { .SetSelectable(true) } else { .SetSelectable(false) .SetTextColor(tcell.GetColor(theme.Grey)) .SetBackgroundColor(tcell.ColorDefault) } := false // scan until machId changes for := .HistoryCursor; < len(.History)-1; ++ { if .History[].MachId != .History[+1].MachId { = true break } } if ! { .SetSelectable(false) .SetTextColor(tcell.GetColor(theme.Grey)) .SetBackgroundColor(tcell.ColorDefault) } // detect clipboard if .clip == nil { .SetTextColor(tcell.GetColor(theme.Grey)) .SetSelectable(false) .SetBackgroundColor(tcell.ColorDefault) .SetTextColor(tcell.GetColor(theme.Grey)) .SetSelectable(false) .SetBackgroundColor(tcell.ColorDefault) } // address := "[" + theme.Grey + "]" if { = "[" + theme.Active + "]" } := .addressBar.GetCell(0, colAddr) if != "" && != "" { := "" if != "" { = "/" + } .SetText( + "mach://[-][::u]" + + "[::-][" + theme.Grey + "]/" + + ) } else if != "" { .SetText( + "mach://[-][::u]" + ) } else { .SetText("[" + theme.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) { if .contentPanels == nil { return } switch .Mach.Switch(states.DebuggerGroups.Views) { 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 := &dbg.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) // TODO optimize: ID registry // msgTx.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) if .params.OutputCallLog && len(.Steps) > 0 { if := .appendCallLog(, , ); != nil { .Mach.AddErr(, nil) } } } func ( *Debugger) ( *Client, *dbg.DbgMsgTx, *types.MsgTxParsed, ) error { := "" := 0 // TODO config := 100 // TODO config := 5000 // TODO parse from steps // for _, step := range msgTx.Steps { // if step.Type != am.StepHandler { // continue // } // // TODO negotiation, other // fields, previous // name, _ := strings.CutPrefix(strings.ReplaceAll( // step.StringFromIndex(c.MsgStruct.StatesIndex), // "*", ""), "handler ") // steps += P.Sprintf("\t%s(e) // t%v \n", // name, // msgTxParsed.TimeSum) // } := false := "\t" for , := range .LogEntries { if !strings.HasPrefix(.Text, "[handler:") { continue } // get name from after first "]" := strings.Index(.Text, "]") if == -1 { continue } := .Text[len("[handler:"):] := .Text[+2:] // skip globals and health if strings.HasPrefix(, am.StateAny) || strings.HasPrefix(, am.StateHealthcheck) || strings.HasPrefix(, am.StateHeartbeat) { continue } // negotiation if (strings.HasSuffix(, am.SuffixEnter) || strings.HasSuffix(, am.SuffixExit)) && ! { = true += "\t{\n" += "\t" // final handlers } else if (strings.HasSuffix(, am.SuffixState) || strings.HasSuffix(, am.SuffixEnd)) && { = false += "\t}\n" = "\t" } // canceled if && !.Accepted { += fmt.Sprintf("%sh%s.%s(e) // => am.Canceled\n", , , ) } else { // accepted += fmt.Sprintf("%sh%s.%s(e)\n", , , ) } ++ } if { += "\t}\n" } if == "" { return nil } = P.Sprintf("\t// t%v\n%s", .TimeSum, ) := path.Join(.params.OutputDir, "call-log", .Id) // TODO paginate, filename suffix with mach time // first file if , := .callLogFiles[.Id]; ! { // init code file := os.MkdirAll(, 0o755) if != nil { return } // clean up on start if := .callLogCleanup(); != nil { return } // create init.go if := .callLogBootstrap(); != nil { return } // first file , := os.Create(path.Join(, "0.go")) if != nil { return } .callLogFiles[.Id] = := callStepsToContent("0", ) .callLogFilesLen[.Id] = int64(len()) if _, = .Write(); != nil { return } .callLogCount[.Id] = // rotation } else if && .callLogCount[.Id] > { := strconv.FormatUint(.TimeSum, 10) , := os.Create(path.Join(, +".go")) if != nil { return } .callLogFiles[.Id] = := callStepsToContent(, ) .callLogFilesLen[.Id] = int64(len()) if _, = .Write(); != nil { return } .callLogCount[.Id] = // append } else { // separator (active states) if .callLogLastSep[.Id] < .callLogCount[.Id]- { += "\n\tactive = am.S{" := .ActiveStates(.MsgStruct.StatesIndex) for , := range { if > 0 { += ", " } += "ss." + } += "}\n" .callLogLastSep[.Id] = .callLogCount[.Id] + } := utils.Sp(` %s } `, ) := []byte() if , := .WriteAt(, .callLogFilesLen[.Id]-3); != nil { return } .callLogFilesLen[.Id] += int64(len() - 3) .callLogCount[.Id] += } return nil } func callStepsToContent( string, string) []byte { := 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 } `, , ) return []byte() } func ( *Debugger) ( string) error { , := os.ReadDir() if != nil { return } for , := range { if .IsDir() { continue } := .Name() if !strings.HasSuffix(, "init.go") { := filepath.Join(, ) if := os.Remove(); != nil { return } } } return nil } // 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 " + .params.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(states.DebuggerGroups.Playing) { case ss.Playing: = formatTxBarTitle("Playing") case ss.TailMode: += formatTxBarTitle("Tail") + " " default: = formatTxBarTitle("Paused") + " " } , := .hGetTxInfo(.CursorTx1-1, ) .currTxBarLeft.SetText() .currTxBarRight.SetText() } := .hNextTxIdx() if > 0 && != nil { := "Next " , := .hGetTxInfo(, ) .nextTxBarLeft.SetText() .nextTxBarRight.SetText() } } func ( *Debugger) () { // check for a ready client := .C if == nil { return } := len(.MsgTxs) := .hNextTx() .timelineSteps.SetTitleColor(cview.Styles.PrimaryTextColor) .timelineSteps.SetFilledColor(cview.Styles.PrimaryTextColor) // grey rejected bars if != nil && !.Accepted { .timelineSteps.SetFilledColor(tcell.GetColor(theme.Grey)) } // mark the last step of a canceled tx in red if != nil && .CursorStep1 == len(.Steps) && !.Accepted { .timelineSteps.SetFilledColor(tcell.GetColor(theme.Err)) } := 0 if != nil { = len(.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), theme.Grey, .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) () { := theme.Inactive if .Mach.IsErr() { = theme.Err } := tcell.GetColor() 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(.params.OutputDir, +".gob.br") , := os.Create() if != nil { log.Printf("Error: export failed %s", ) return } defer .Close() // prepare the format := time.Now() if .PrevTx() != nil { = *.C.Tx(max(0, .C.CursorTx1-1)).Time } := make([]*server.Exportable, 0, len(.Clients)) := 0 for , := range .Clients { // omit disconnected if .Mach.Is1(ss.FilterDisconn) && !.Connected.Load() { continue } // omit rpc & relay if .Mach.Is1(ss.FilterRpcMachs) && machIsRpc(.MsgStruct) { continue } = append(, &server.Exportable{ MsgStruct: .Exportable.MsgStruct, MsgTxs: .Exportable.MsgTxs, Version: utils.GetVersion(), }) // snapshot limits to a single tx if { [].MsgTxs = []*dbg.DbgMsgTx{.Tx(.TxAtHTime())} } ++ } // create a new brotli writer := brotli.NewWriter() defer .Close() // encode := gob.NewEncoder() = .Encode() if != nil { log.Printf("Error: export failed %s", ) } } func ( *Debugger) ( int, string) (string, string) { := .C.MsgTxs[] := .C.MsgTxsParsed[] := := " " if == nil { return , } // left side var *dbg.DbgMsgTx := .hFilterTxCursor1(.C, , true) - 1 if > 0 { = .C.MsgTxs[] } // TODO limit state names to a group when [2]group := .CalledStateNames(.C.MsgStruct.StatesIndex) += P.Sprintf(" | tx: %d", ) if .TimeDiff == 0 { += " | Time: [" + theme.Grey + "] 0[-]" } else { += P.Sprintf(" | Time: +%d", .TimeDiff) } += " |" := "" if len() == 1 && .C.MsgStruct.States[[0]].Multi { += " multi" } if !.Accepted { += "[" + theme.Grey + "]" } := "" if .IsQueued { = "q" } += fmt.Sprintf(" %s%s%s: [::b]%s[::-]", , .Type, , strings.Join(, ", ")) if !.Accepted { += "[-]" } // right side if .IsAuto { += "auto | " } if .IsCheck { += "check | " } if !.Accepted { += "[" + theme.Grey + "]canceled[-] | " } // format time := .Time.Format(timeFormat) if != nil { := .Time.Format(timeFormat) if := findFirstDiff(, ); != -1 { = [:] + "[" + theme.White + "]" + [:+1] + "[" + theme.Grey + "]" + [+1:] } } += fmt.Sprintf( "add: %d | rm: %d | touch: %3s | ["+theme.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 { .hRemoveClient(.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 *dbg.DbgMsgTx var *dbg.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( tcell.GetColor(theme.Highlight3), ) = } } 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.GetColor(theme.Grey)) } // mark called states if slices.Contains(, ) { .SetAttributes( tcell.AttrBold | tcell.AttrUnderline, ) } // mark selected state if .C.SelectedState == { .SetBackgroundColor(tcell.GetColor(theme.Highlight3)) } } 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(tcell.GetColor(theme.Highlight3)) } if == 0 { .SetTextColor(tcell.GetColor(theme.Grey)) 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, Pass(&A{ 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(tcell.GetColor(theme.Highlight)) } // mark called states if slices.Contains(, ) { .SetAttributes(tcell.AttrUnderline) } if == .CursorTx1 { // current tx .SetBackgroundColor(tcell.GetColor(theme.Highlight3)) } else if .C.SelectedState == { // mark selected state .SetBackgroundColor(tcell.GetColor(theme.Highlight3)) } if ( || == am.StateException) && .Is1(, ) { // mark exceptions if .Accepted { .SetBackgroundColor(tcell.GetColor(theme.ErrBg)) } else { .SetBackgroundColor(tcell.GetColor(theme.Highlight3)) } } } // timestamp := .Time.Format(timeFormat) := // highlight first diff number since prev timestamp if > 1 { := .MsgTxs[-2].Time.Format(timeFormat) if := findFirstDiff(, ); != -1 { = [:] + "[" + theme.White + "]" + [:+1] + "[" + theme.Grey + "]" + [+1:] } } // tail cell .matrix.SetCellSimple(, len(), fmt.Sprintf( " ["+theme.Grey+"]%d | %s[-]", , , )) := .matrix.GetCell(, len()) // current tx if == .CursorTx1 { .SetBackgroundColor(tcell.GetColor(theme.Highlight3)) } } := 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() } } var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} func ( *Debugger) () { .statusBarLeft.SetText("") .statusBarRight.SetText("") := .C if == nil { return } := .hCurrentTx() := " " if .Mach.Is1(ss.Loading) { = spinnerFrames[.loadingPos] } // left // current global mach time var uint64 var time.Time // current tx of the selected client := .hCurrentTx() if != nil { = .lastScrolledTxTime if .IsZero() { = *.Time } } for , := range .Clients { := .TxAtHTime() if != -1 { := .MsgTxsParsed[] += .TimeSum } } := []string{P.Sprintf("%sGraph:t%v", , )} // selected state := slices.Index(.MsgStruct.StatesIndex, .SelectedState) if != -1 { = append(, "[::b]"+.SelectedState+"[::-]", fmt.Sprintf("idx: %d", )) if != nil && len(.Clocks) > { = append(, fmt.Sprintf("tick: %d", .Clocks[])) } // TODO show schema group / inheritance } .statusBarLeft.SetText(strings.Join(, " ["+theme.Grey+"]|[-] ")) // right := "" if .CursorStep1 > 0 { := .hNextTx() if != nil && .Steps != nil { := min(len(.Steps)-1, .CursorStep1-1) := .Steps[] = .StringFromIndex(.MsgStruct.StatesIndex) } } // markdown to cview TODO extract := 0 for strings.Contains(, "**") { := "[::b]" if %2 == 1 { = "[::-]" } ++ = strings.Replace(, "**", , 1) } .statusBarRight.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) () *types.Filters { := .Mach.Is1 return &types.Filters{ SkipCanceledTx: (ss.FilterCanceledTx), SkipAutoTx: (ss.FilterAutoTx), SkipAutoCanceledTx: (ss.FilterAutoCanceledTx), SkipEmptyTx: (ss.FilterEmptyTx), SkipHealthTx: (ss.FilterHealth), SkipQueuedTx: (ss.FilterQueuedTx), SkipOutGroup: (ss.FilterOutGroup), SkipChecks: (ss.FilterChecks), SkipRpcMach: (ss.FilterRpcMachs), } } func ( *Debugger) ( *types.Filters) { := .Mach.Add1 := .Mach.Remove1 if .SkipCanceledTx { (ss.FilterCanceledTx, nil) } else { (ss.FilterCanceledTx, nil) } if .SkipAutoTx { (ss.FilterAutoTx, nil) } else { (ss.FilterAutoTx, nil) } if .SkipAutoCanceledTx { (ss.FilterAutoCanceledTx, nil) } else { (ss.FilterAutoCanceledTx, nil) } if .SkipEmptyTx { (ss.FilterEmptyTx, nil) } else { (ss.FilterEmptyTx, nil) } if .SkipHealthTx { (ss.FilterHealth, nil) } else { (ss.FilterHealth, nil) } if .SkipQueuedTx { (ss.FilterQueuedTx, nil) } else { (ss.FilterQueuedTx, nil) } if .SkipOutGroup { (ss.FilterOutGroup, nil) } else { (ss.FilterOutGroup, nil) } if .SkipChecks { (ss.FilterChecks, nil) } else { (ss.FilterChecks, nil) } if .SkipRpcMach { (ss.FilterRpcMachs, nil) } else { (ss.FilterRpcMachs, nil) } } // filtersActive checks if any filters are active. func ( *Debugger) () bool { return .Mach.Any1(states.DebuggerGroups.Filters...) } // hFilterTx returns true when a TX passes selected toolbarItems. func ( *Debugger) ( *Client, int, *types.Filters) bool { := .MsgTxs[] := .MsgTxsParsed[] := .CalledStateNames(.MsgStruct.StatesIndex) := .SelectedGroup := // basic filters if .SkipAutoTx && .IsAuto { return false } else if .SkipAutoCanceledTx && .IsAuto && !.Accepted { return false } else if .SkipAutoCanceledTx && .IsAuto && .IsQueued { // check if this queued tx got canceled later := .TxExecutedBy() if != nil && !.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.TxAtHTime() if == -1 { return false } if { = .hFilterTxCursor1(.C, , true) } .hSetCursor1(, &A{ Cursor1: , FilterBack: true, }) return true } func ( *Debugger) ( *Client, []string) []string { , := .Clients[.MsgStruct.Parent] if ! { return } = slices.Concat(, .MsgStruct.Tags) return .(, ) } func ( *Debugger) () { switch .params.ViewTimelines { case types.ParamsViewTimelinesNone: .Mach.Add(S{ss.TimelineTxHidden, ss.TimelineStepsHidden}, nil) case types.ParamsViewTimelinesOne: .Mach.Add1(ss.TimelineStepsHidden, nil) .Mach.Remove1(ss.TimelineTxHidden, nil) case types.ParamsViewTimelinesTwo: .Mach.Remove(S{ss.TimelineStepsHidden, ss.TimelineTxHidden}, nil) } } func ( *Debugger) () tcell.Color { := cview.Styles.MoreContrastBackgroundColor if .Mach.IsErr() { = tcell.GetColor(theme.Err) } return } func ( *Debugger) () string { := .Mach.NewStateCtx(ss.LogReaderVisible) , := amhelp.EvalGetter(, "LogReaderText", 3, .Mach, func() (string, error) { return treeToText(.logReader), nil }) return } func ( *Debugger) ( string) { // TODO cant be scheduled, as the client can connect in the meantime // d.Add1(ss.RemoveClient, am.A{"Client.id": c.id}) delete(.Clients, ) .hRemoveHistory() delete(.callLogLastSep, ) delete(.callLogCount, ) delete(.callLogFiles, ) delete(.callLogFilesLen, ) // TODO remove from graph? } func ( *Debugger) ( string) error { := 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 ) `) := filepath.Join(, "init.go") if , := os.Stat(); != nil && os.IsNotExist() { return os.WriteFile(, []byte(), 0o644) } else if != nil { return } return nil }