package debugger
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"slices"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coder/websocket"
amgraph "github.com/pancsta/asyncmachine-go/pkg/graph"
amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
"github.com/pancsta/asyncmachine-go/pkg/telemetry/dbg"
"github.com/pancsta/asyncmachine-go/tools/debugger/types"
amvis "github.com/pancsta/asyncmachine-go/tools/visualizer"
)
var _ = ss .WebReqDiag
func (d *Debugger ) WebReqDiagState (e *am .Event ) {
args := am .ParseArgs [A ](e .Args )
r := args .HttpRequest
w := args .HttpResponseWriter
diagType := args .DiagType
done := args .DoneChan
defer close (done )
name := diagType .Value
uri := r .RequestURI
switch {
case uri == "/viewer/" +name :
html := string (amvis .HtmlDiagram )
html = strings .ReplaceAll (html , "localhost:6831" , d .listenAddrHttp )
html = strings .ReplaceAll (html , `const TYPE = "mach";` ,
fmt .Sprintf (`const TYPE = "%s";` , name ))
_ , err := w .Write ([]byte (html ))
d .Mach .EvAddErrState (e , ss .ErrWeb , err , nil )
case strings .HasPrefix (uri , "/viewer/" +name +".svg" ):
svgPath := filepath .Join (d .params .OutputDir , "am-vis-" +name +".svg" )
b , err := os .ReadFile (svgPath )
if name != "state" && *d .selectedState .Load () != "" {
d .Mach .EvAddErrState (e , ss .ErrWeb , err , nil )
}
if err != nil {
return
}
w .Header ().Set ("Content-Type" , "image/svg+xml" )
_, err = w .Write (b )
d .Mach .EvAddErrState (e , ss .ErrWeb , err , nil )
}
}
var _ = ss .WebSocketDiag
func (d *Debugger ) WebSocketDiagState (e *am .Event ) {
mach := d .Mach
ctx , cancel := context .WithCancel (mach .NewStateCtx (ss .Start , e ))
args := am .ParseArgs [A ](e .Args )
ws := args .WebSocketConn
r := args .HttpRequest
done := args .DoneChan
if d .Params .Load ().OutputDiagrams == types .ParamsOutputDiagramsNone {
ws .Close (websocket .StatusNormalClosure , "Diagrams disabled" )
cancel ()
return
}
var progressState string
var readyState string
var updateChan chan struct {}
switch args .DiagType {
case types .DiagramTypeMach :
progressState = ss .DiagramsMachRendering
updateChan = d .diagMachUpdate
go func () {
d .Mach .EvAddErrState (e , ss .ErrDiagrams ,
d .diagramsMachUpdating (ctx ), nil )
}()
case types .DiagramTypeGraph :
progressState = ss .DiagramsGraphRendering
readyState = ss .DiagramsGraphReady
case types .DiagramTypeState :
progressState = ss .DiagramsStatesRendering
updateChan = d .diagStateUpdate
go func () {
d .Mach .EvAddErrState (e , ss .ErrDiagrams ,
d .diagramsStateUpdating (ctx ), nil )
}()
case types .DiagramTypeSteps :
updateChan = d .diagStepsUpdate
d .hDiagramsStepsRendering (ctx , d .hCurrentTx (), d .hCurrentTxParsed ())
}
mach .Go (ctx , func () {
defer close (done )
if updateChan != nil {
d .diagramWsUpdateChan (ctx , updateChan , progressState , ws , r )
return
}
d .diagramsWsUpdateStates (ctx , progressState , ws , r , readyState )
})
mach .Fork (ctx , e , func () {
for {
_ , _ , err := ws .Read (r .Context ())
if err != nil {
mach .Log ("websocket closed" )
if websocket .CloseStatus (err ) == -1 {
err = fmt .Errorf ("websocket read: %w" , err )
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
}
cancel ()
return
}
}
})
}
var _ = ss .DiagramsGraphRendering
func (d *Debugger ) DiagramsGraphRenderingEnter (e *am .Event ) bool {
return len (d .Clients ) > 0 &&
d .params .OutputDiagrams != types .ParamsOutputDiagramsNone
}
func (d *Debugger ) DiagramsGraphRenderingState (e *am .Event ) {
ctx := d .Mach .NewStateCtx (ss .DiagramsGraphRendering , e )
d .hUpdateGraphHash ()
graphHash := d .graphHash
diagDir := path .Join (d .params .OutputDir , "diagrams" )
snapshot , err := d .graph .Clone ()
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
d .Mach .Fork (ctx , e , func () {
svgName := fmt .Sprintf ("graph-%s" , graphHash )
svgPath := path .Join (diagDir , svgName )
if _ , err := os .Stat (svgPath + ".svg" ); err != nil {
vis := amvis .NewRenderer (snapshot , d .Mach .Log )
amvis .PresetMap (vis )
vis .OutputFilename = svgPath
err := vis .GenDiagrams (ctx )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
}
_ = d .diagramsLink (svgName , types .DiagramTypeGraph )
d .Mach .EvAdd1 (e , ss .DiagramsGraphReady , nil )
})
}
var _ = ss .DiagramsMachRendering
func (d *Debugger ) DiagramsMachRenderingEnter (e *am .Event ) bool {
return d .params .OutputDiagrams != types .ParamsOutputDiagramsNone &&
d .C != nil
}
func (d *Debugger ) DiagramsMachRenderingState (e *am .Event ) {
ctx := d .Mach .NewStateCtx (ss .DiagramsMachRendering , e )
lvl := d .params .OutputDiagrams .Value
c := d .C
diagName := fmt .Sprintf ("mach-%s-%d-%s" , c .Id , lvl , c .SchemaHash )
shot , err := d .graph .Clone ()
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
var states S
if g := c .SelectedGroup ; g != "" &&
d .params .OutputDiagGroup == types .ParamsOutDiagGroupSkip {
states = c .MsgSchemaParsed .Groups [g ]
diagName = fmt .Sprintf ("mach-%s-%s-%d-%s" ,
c .Id , types .NormalizeGroupName (g ), lvl , c .SchemaHash )
}
d .Mach .Fork (ctx , e , func () {
err := d .diagramsMachRender (ctx , shot , c .Id , lvl , diagName , states )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
_ = d .diagramsLink (diagName , types .DiagramTypeMach )
file , err := os .Open (d .diagramsDiagPath (diagName ) + ".svg" )
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
dom , err := goquery .NewDocumentFromReader (file )
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
d .Mach .EvAdd1 (e , ss .DiagramsMachReady , Pass (&A {
DiagDom : dom ,
DiagName : diagName ,
}))
})
}
var _ = ss .DiagramsMachReady
func (d *Debugger ) DiagramsMachReadyState (e *am .Event ) {
ctx := d .Mach .NewStateCtx (ss .DiagramsMachReady )
args := am .ParseArgs [A ](e .Args )
d .diagMachDom .Store (args .DiagDom )
d .diagMachName .Store (&args .DiagName )
d .Mach .Go (ctx , func () {
d .Mach .EvAddErrState (e , ss .ErrDiagrams ,
d .diagramsMachUpdating (ctx ), nil )
})
}
var _ = ss .DiagramsStatesRendering
func (d *Debugger ) DiagramsStatesRenderingEnter (e *am .Event ) bool {
return d .params .OutputDiagrams != types .ParamsOutputDiagramsNone &&
d .C != nil
}
func (d *Debugger ) DiagramsStatesRenderingState (e *am .Event ) {
ctx := d .Mach .NewStateCtx (ss .DiagramsStatesRendering )
c := d .C
diagDir := path .Join (d .params .OutputDir , "diagrams" )
index := c .MsgStruct .StatesIndex
groupName := c .SelectedGroup
groupOnly := d .params .OutputDiagGroup != types .ParamsOutDiagGroupNone
hash := c .SchemaHash
var allowStates S
if groupName != "" {
allowStates = c .MsgSchemaParsed .Groups [groupName ]
}
machId := c .Id
schema := c .MsgStruct .States .Clone ()
d .Mach .Go (ctx , func () {
for _ , state := range index {
if ctx .Err () != nil {
return
}
if state == am .StateStart || state == am .StateReady {
continue
}
diagName := d .diagramsStateDiagName (
machId , state , hash , groupName , groupOnly ,
)
err := d .diagramsStateRender (ctx , schema , state ,
diagName , diagDir , allowStates )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
if *d .selectedState .Load () == state {
d .diagramsStatesPush (ctx , d .diagramsDiagPath (diagName ))
}
}
d .Mach .EvAdd1 (e , ss .DiagramsStatesReady , nil )
})
}
func (d *Debugger ) hDiagramsStepsRendering (
ctx context .Context , tx *dbg .DbgMsgTx , parsed *types .MsgTxParsed ,
) {
if tx == nil {
return
}
c := d .C
e := am .CtxToEv (ctx )
index := c .MsgStruct .StatesIndex
_ = d .txFileMd .Truncate (0 )
_ = d .diagStepsFileD2 .Truncate (0 )
_ = d .diagStepsFileD2Svg .Truncate (0 )
_ = d .diagStepsFileMermaid .Truncate (0 )
_ = d .diagStepsFileMermaidAscii .Truncate (0 )
_, _ = d .txFileMd .WriteAt ([]byte (tx .TxString (index )), 0 )
if tx .Steps == nil {
return
}
var group am .S
if d .params .OutputDiagGroup != types .ParamsOutDiagGroupNone &&
c .SelectedGroup != "" {
group = c .MsgSchemaParsed .Groups [c .SelectedGroup ]
}
visTx := amvis .Transition {
Log : amhelp .MachToSlog (d .Mach ),
Tx : tx ,
TxParsed : parsed ,
PrevTx : d .hPrevTx (),
StateTrace : d .hStateTrace (tx ),
Group : group ,
}
d .Mach .Go (ctx , func () {
if err := d .diagramsStepsRender (ctx , visTx , index ); err != nil {
if ctx .Err () != nil {
return
}
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
}
select {
case d .diagStepsUpdate <- struct {}{}:
default :
}
})
}
func (d *Debugger ) hDiagramsFilters (
c *Client , tx *dbg .DbgMsgTx ,
) *amvis .Filters {
index := c .MsgStruct .StatesIndex
diagFilters := &amvis .Filters {
MachId : c .Id ,
Index : index ,
Selected : am .S {d .C .SelectedState },
}
if tx != nil {
diagFilters .Active = tx .ActiveStates (index )
if d .params .OutputDiagTx != types .ParamsOutDiagTxNone {
parsed := c .MsgTxsParsed [c .CursorTx1 -1 ]
showIdxs := tx .CalledStatesIdxs
switch d .params .OutputDiagTx {
case types .ParamsOutDiagTxMutated :
showIdxs = slices .Concat (parsed .StatesAdded , parsed .StatesRemoved )
case types .ParamsOutDiagTxTouched :
fallthrough
case types .ParamsOutDiagTxRelations :
showIdxs = parsed .StatesTouched
}
diagFilters .Highlighted = c .IndexesToStates (showIdxs )
}
if d .params .OutputDiagTx == types .ParamsOutDiagTxRelations {
for _ , step := range tx .Steps {
if step .Type != am .StepRelation {
continue
}
diagFilters .HighlightedRels = append (diagFilters .HighlightedRels ,
[3 ]string {
step .GetFromState (index ),
step .GetToState (index ),
step .RelType .String (),
})
}
}
}
if d .params .OutputDiagGroup == types .ParamsOutDiagGroupHide &&
c .SelectedGroup != "" {
diagFilters .Visible = c .MsgSchemaParsed .Groups [c .SelectedGroup ]
}
return diagFilters
}
func (d *Debugger ) diagramsMachUpdating (ctx context .Context ) error {
if d .Params .Load ().OutputDiagrams == types .ParamsOutputDiagramsNone {
return nil
}
if d .diagMachDom .Load () == nil {
return nil
}
select {
case <- ctx .Done ():
return nil
case <- d .Mach .When1 (ss .DiagramsMachReady , ctx ):
}
diagFilters , err := d .diagramsMachFilters (ctx )
if ctx .Err () != nil {
return nil
}
if err != nil {
return err
}
diagPath := d .diagramsDiagPath (*d .diagMachName .Load ())
err = amvis .UpdateCache (ctx , diagPath +".svg" ,
d .diagMachDom .Load (), diagFilters )
if ctx .Err () != nil {
return nil
}
if err != nil {
return err
}
select {
case d .diagMachUpdate <- struct {}{}:
default :
}
return nil
}
func (d *Debugger ) diagramsStateUpdating (ctx context .Context ) error {
if d .Params .Load ().OutputDiagrams == types .ParamsOutputDiagramsNone {
return nil
}
state := *d .selectedState .Load ()
machId := *d .selectedClient .Load ()
group := *d .selectedGroup .Load ()
schemaHash := *d .selectedSchemaHash .Load ()
groupOnly := d .params .OutputDiagGroup != types .ParamsOutDiagGroupNone
diagName := d .diagramsStateDiagName (machId , state , schemaHash ,
group , groupOnly )
if err := d .diagramsLink (diagName , types .DiagramTypeState ); err != nil {
return err
}
diagPath := d .diagramsDiagPath (diagName )
if _ , err := os .Stat (diagPath + ".svg" ); err == nil {
d .diagramsStatesPush (ctx , diagPath )
} else {
select {
case d .diagStateUpdate <- struct {}{}:
default :
}
}
return nil
}
func (d *Debugger ) diagramsMachFilters (
ctx context .Context ,
) (*amvis .Filters , error ) {
return amhelp .EvalGetter (ctx , "diagramsFilters" , 3 , d .Mach ,
func () (*amvis .Filters , error ) {
tx := d .hCurrentTx ()
return d .hDiagramsFilters (d .C , tx ), nil
})
}
func (d *Debugger ) diagramWsUpdateChan (
ctx context .Context , updateChan chan struct {}, renderingState string ,
ws *websocket .Conn , r *http .Request ,
) {
e := am .CtxToEv (ctx )
mach := d .Mach
for {
if d .Mach .Is1 (renderingState ) {
time .Sleep (100 * time .Millisecond )
if d .Mach .Is1 (renderingState ) {
msg , err := json .Marshal (types .WsDiagMsg {
Event : "loading" ,
})
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
continue
}
err = ws .Write (r .Context (), websocket .MessageText , msg )
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
return
}
}
}
select {
case <- ctx .Done ():
return
case <- updateChan :
machId := ""
if addr := d .MachAddr (); addr != nil {
machId = addr .MachId
}
msg , err := json .Marshal (types .WsDiagMsg {
Event : "refresh" ,
Id : machId ,
})
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
continue
}
err = ws .Write (r .Context (), websocket .MessageText , msg )
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
return
}
continue
}
}
}
func (d *Debugger ) diagramsWsUpdateStates (
ctx context .Context , progressState string , ws *websocket .Conn ,
r *http .Request , readyState string ,
) {
e := am .CtxToEv (ctx )
mach := d .Mach
var ctxStep context .Context
var cancelStep context .CancelFunc
var lastReady uint64
for {
if cancelStep != nil {
cancelStep ()
}
ctxStep , cancelStep = context .WithCancel (ctx )
defer cancelStep ()
tickReady := d .Mach .Tick (readyState )
if lastReady == tickReady {
if progressState != "" {
select {
case <- ctx .Done ():
return
case <- mach .When1 (progressState , ctxStep ):
}
}
}
lastReady = tickReady
msg , err := json .Marshal (types .WsDiagMsg {
Event : "loading" ,
})
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
continue
}
err = ws .Write (r .Context (), websocket .MessageText , msg )
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
return
}
if readyState != "" {
select {
case <- ctx .Done ():
return
case <- mach .When1 (readyState , ctxStep ):
}
}
msg , err = json .Marshal (types .WsDiagMsg {
Event : "refresh" ,
Id : d .MachAddr ().MachId ,
Group : *d .diagSkipGroup .Load (),
})
if err != nil {
mach .Log (err .Error())
continue
}
err = ws .Write (r .Context (), websocket .MessageText , msg )
if err != nil {
mach .EvAddErrState (e , ss .ErrWeb , err , nil )
return
}
}
}
func (d *Debugger ) diagramsMachRender (
ctx context .Context , shot *amgraph .Graph , machId string ,
detailLvl int , diagName string , states S ,
) error {
diagPath := d .diagramsDiagPath (diagName )
if _ , err := os .Stat (diagPath + ".svg" ); err == nil {
return nil
}
vis := amvis .NewRenderer (shot , d .Mach .Log )
amvis .PresetSingle (vis )
switch detailLvl {
default :
return fmt .Errorf ("unknown diagram detail level: %d" , detailLvl )
case 1 :
vis .RenderNestSubmachines = false
vis .RenderStart = false
vis .RenderInherited = false
vis .RenderPipes = false
vis .RenderHalfPipes = false
vis .RenderHalfConns = false
vis .RenderHalfHierarchy = false
vis .RenderParentRel = false
case 2 :
vis .RenderNestSubmachines = false
vis .RenderStart = true
vis .RenderInherited = true
vis .RenderPipes = false
vis .RenderHalfPipes = false
vis .RenderHalfConns = false
vis .RenderHalfHierarchy = false
case 3 :
vis .RenderNestSubmachines = true
vis .RenderStart = true
vis .RenderInherited = true
vis .RenderPipes = true
}
d .Mach .Log ("rendering graphs lvl %d" , detailLvl )
vis .RenderActive = false
vis .RenderMachs = []string {machId }
vis .OutputFilename = diagPath
if len (states ) > 0 {
vis .RenderAllowlist = states
}
return vis .GenDiagrams (ctx )
}
func (d *Debugger ) diagramsStateRender (
ctx context .Context , schema am .Schema , state string , diagName string ,
diagDir string , allowStates S ,
) error {
diagPath := d .diagramsDiagPath (diagName )
_ , err := os .Stat (diagPath + ".svg" )
if err == nil {
return nil
}
e := am .CtxToEv (ctx )
shot , err := d .graph .Clone ()
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return nil
}
direct := schema .AdjacentStates (state )
show := S {state }.Add (direct )
for _ , s2 := range direct {
if s2 == am .StateReady || s2 == am .StateStart {
continue
}
show = show .Add (schema .AdjacentStates (s2 ))
}
show = show .Unique ()
if allowStates != nil {
show = slices .DeleteFunc (show , func (s string ) bool {
return !slices .Contains (allowStates , s )
})
}
if len (show ) == 0 {
return nil
}
if ctx .Err () != nil {
return nil
}
vis := amvis .NewRenderer (shot , d .Mach .Log )
amvis .PresetSingle (vis )
vis .RenderNestSubmachines = false
vis .RenderStart = false
vis .RenderActive = false
vis .RenderReady = false
vis .RenderInherited = true
vis .RenderPipes = true
vis .RenderHalfPipes = false
vis .RenderHalfConns = false
vis .RenderHalfHierarchy = false
vis .RenderParentRel = false
vis .RenderZoom = S {show [0 ]}
vis .RenderMachTitles = false
vis .RenderAllowlist = show
vis .RenderMachs = []string {d .C .Id }
vis .OutputMermaid = false
vis .OutputFilename = diagPath
return vis .GenDiagrams (ctx )
}
func (d *Debugger ) diagramsStepsRender (
ctx context .Context , visTx amvis .Transition , index am .S ,
) error {
e := am .CtxToEv (ctx )
var err error
d2Diag , d2Svg , errD2 := visTx .D2 (ctx , index )
if ctx .Err () != nil {
return ctx .Err ()
}
err = errors .Join (err , errD2 )
mermaid , ascii , errMer := visTx .Mermaid (index )
if ctx .Err () != nil {
return ctx .Err ()
}
err = errors .Join (err , errMer )
d .Mach .Eval ("diagramsStepsRender" , func () {
if errD2 == nil {
_ , err := d .diagStepsFileD2 .WriteAt ([]byte (d2Diag ), 0 )
if err != nil {
d .Mach .EvAddErr (e , err , nil )
} else {
_, _ = d .diagStepsFileD2Svg .WriteAt (d2Svg , 0 )
}
}
if errMer == nil {
_ , err := d .diagStepsFileMermaid .WriteAt ([]byte (mermaid ), 0 )
if err != nil {
d .Mach .EvAddErr (e , err , nil )
} else {
_, _ = d .diagStepsFileMermaidAscii .WriteAt ([]byte (ascii ), 0 )
}
}
}, ctx )
return errors .Join (
err ,
d .diagramsLink ("steps.d2" , types .DiagramTypeSteps ),
)
}
func (d *Debugger ) diagramsLink (
diagName string , diagType types .DiagramType ,
) error {
dir := d .Params .Load ().OutputDir
diag := diagType .Value
source := path .Join (dir , "am-vis-" +diag +".svg" )
target := path .Join ("diagrams" , diagName +".svg" )
_ = os .Remove (source )
d .Mach .Log ("diaglink %s -> %s" , source , target )
return os .Symlink (target , source )
}
func (d *Debugger ) diagramsDiagPath (name string ) string {
return path .Join (d .Params .Load ().OutputDir , "diagrams" , name )
}
func (d *Debugger ) diagramsStateDiagName (
machId , state string , hash string , groupName string , groupOnly bool ,
) string {
name := fmt .Sprintf ("state-%s-%s-%s" , machId , state , hash )
if groupName != "" && groupOnly {
name = fmt .Sprintf ("state-%s-%s-%s-%s" ,
machId , types .NormalizeGroupName (groupName ), state , hash )
}
return name
}
func (d *Debugger ) diagramsStatesPush (ctx context .Context , diagPath string ) {
defer func () {
select {
case d .diagStateUpdate <- struct {}{}:
default :
}
}()
e := am .CtxToEv (ctx )
file , err := os .Open (diagPath + ".svg" )
if err != nil {
return
}
var dom *goquery .Document
if *d .diagStatePath .Load () == diagPath {
dom = d .diagStateDom .Load ()
} else {
dom , err = goquery .NewDocumentFromReader (file )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
d .diagStateDom .Store (dom )
d .diagStatePath .Store (&diagPath )
}
diagMachFilters , err := d .diagramsMachFilters (ctx )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
diagStepFilters := &amvis .Filters {
Active : diagMachFilters .Active ,
Index : diagMachFilters .Index ,
}
err = amvis .UpdateCache (ctx , diagPath +".svg" , dom , diagStepFilters )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
}
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 .