@@ -24,40 +24,25 @@ import (
24
24
"github.com/mindersec/minder/pkg/engine/v1/interfaces"
25
25
)
26
26
27
- func MakeEventHandler (ch chan <- * debug.Event ) func (debug.Event ) {
27
+ func makeEventHandler (ch chan <- * debug.Event ) func (debug.Event ) {
28
28
return func (event debug.Event ) {
29
29
ch <- & event
30
30
}
31
31
}
32
32
33
- func MakeTracingEventHandler (ch chan <- * debug.Event ) func (debug.Event ) {
33
+ //nolint:unused
34
+ func makeTracingEventHandler (ch chan <- * debug.Event ) func (debug.Event ) {
34
35
return func (event debug.Event ) {
35
36
fmt .Fprintf (os .Stderr , "%+v\n " , event )
36
37
ch <- & event
37
38
}
38
39
}
39
40
40
- func (ds * debugSession ) WaitFor (
41
- ctx context.Context ,
42
- eventTypes ... debug.EventType ,
43
- ) * debug.Event {
44
- for {
45
- select {
46
- case e := <- ds .ch :
47
- if slices .Contains (eventTypes , e .Type ) {
48
- return e
49
- }
50
- case <- ctx .Done ():
51
- return nil
52
- }
53
- }
54
- }
55
-
56
41
var (
57
42
errEmptySource = errors .New ("empty source code" )
58
- errInvalidInput = errors .New ("invalid input" )
59
43
errInvalidInstr = errors .New ("invalid instruction" )
60
44
errInvalidBP = errors .New ("invalid breakpoint" )
45
+ errUserAbort = errors .New ("user abort" )
61
46
)
62
47
63
48
// Debug implements an interactive debugger for REGO-based evaluators.
@@ -67,9 +52,6 @@ func (e *Evaluator) Debug(
67
52
input * Input ,
68
53
funcs ... func (* rego.Rego ),
69
54
) error {
70
- ctx , cancel := context .WithCancel (ctx )
71
- defer cancel ()
72
-
73
55
allOpts := make ([]func (* rego.Rego ), 0 , len (e .regoOpts )+ len (funcs ))
74
56
allOpts = append (allOpts , e .regoOpts ... )
75
57
allOpts = append (allOpts , funcs ... )
@@ -80,7 +62,7 @@ func (e *Evaluator) Debug(
80
62
withInput (input ),
81
63
withQuery (e .reseval .getQueryString ()),
82
64
withOpts (allOpts ... ),
83
- withTracingEventHandler (),
65
+ // withTracingEventHandler(),
84
66
)
85
67
if err != nil {
86
68
return fmt .Errorf ("error initializing debugger: %w" , err )
@@ -93,7 +75,7 @@ type debugSession struct {
93
75
prompt string
94
76
src string
95
77
lines int
96
- input * Input
78
+ input any
97
79
query string
98
80
opts []debug.LaunchOption
99
81
ch chan * debug.Event
@@ -125,11 +107,7 @@ func withSource(src string) debugSessionOption {
125
107
126
108
func withInput (input any ) debugSessionOption {
127
109
return func (ds * debugSession ) error {
128
- inner , ok := input .(* Input )
129
- if ! ok {
130
- return fmt .Errorf ("%w: wrong type %T" , errInvalidInput , input )
131
- }
132
- ds .input = inner
110
+ ds .input = input
133
111
return nil
134
112
}
135
113
}
@@ -159,11 +137,20 @@ func withOpts(opts ...func(*rego.Rego)) debugSessionOption {
159
137
}
160
138
}
161
139
140
+ //nolint:unused
162
141
func withTracingEventHandler () debugSessionOption {
163
142
return func (ds * debugSession ) error {
143
+ // NOTE: this channel must be buffered, because REGO
144
+ // interpreter emits several events that we're
145
+ // currently handling in the same thread of execition
146
+ // of the CLI interface.
147
+ //
148
+ // The solution would be handling CLI events and
149
+ // debuggee events asynchronously, but we're not there
150
+ // yet.
164
151
ch := make (chan * debug.Event , 10 )
165
152
ds .ch = ch
166
- ds .handler = MakeTracingEventHandler (ch )
153
+ ds .handler = makeTracingEventHandler (ch )
167
154
return nil
168
155
}
169
156
}
@@ -180,20 +167,62 @@ func newDebugSession(
180
167
}
181
168
182
169
if ds .handler == nil {
170
+ // NOTE: this channel must be buffered, because REGO
171
+ // interpreter emits several events that we're
172
+ // currently handling in the same thread of execition
173
+ // of the CLI interface.
174
+ //
175
+ // The solution would be handling CLI events and
176
+ // debuggee events asynchronously, but we're not there
177
+ // yet.
183
178
ch := make (chan * debug.Event , 10 )
184
179
ds .ch = ch
185
- ds .handler = MakeEventHandler (ch )
180
+ ds .handler = makeEventHandler (ch )
186
181
}
187
182
188
183
return ds , nil
189
184
}
190
185
186
+ func (ds * debugSession ) waitFor (
187
+ ctx context.Context ,
188
+ eventTypes ... debug.EventType ,
189
+ ) * debug.Event {
190
+ for {
191
+ select {
192
+ case e := <- ds .ch :
193
+ if slices .Contains (eventTypes , e .Type ) {
194
+ return e
195
+ }
196
+ case <- ctx .Done ():
197
+ return nil
198
+ }
199
+ }
200
+ }
201
+
191
202
func (ds * debugSession ) startDebugger (
192
203
ctx context.Context ,
193
204
) error {
194
205
debugger := debug .NewDebugger (
195
206
debug .SetEventHandler (ds .handler ),
196
207
)
208
+ // This combination of flags provides roughly the same user
209
+ // experience as one would have while debugging imperative
210
+ // languages using a standard debugger like lldb or gdb.
211
+ //
212
+ // Specifically, `StopOnEntry` stops when entering an
213
+ // expression, which is like stepping through some, but not
214
+ // all, lines and even inside the same line multiple times in
215
+ // the case of list/set comprehensions, while `StopOnFail`
216
+ // results in stopping at all expressions producing a `false`
217
+ // value, which is similar to the previous case in that it
218
+ // stops every time a check fails during a list/set
219
+ // comprehension.
220
+ //
221
+ // The previous descriptions must be taken with a grain of
222
+ // salt and are likely missing useful cases. That said, the
223
+ // described cases are hardly seen when debugging imperative
224
+ // languages, which is the user experience we want to provide
225
+ // at the moment. Of course, this might change in the future.
197
226
launchProps := debug.LaunchEvalProperties {
198
227
LaunchProperties : debug.LaunchProperties {
199
228
StopOnEntry : false ,
@@ -242,11 +271,11 @@ func (ds *debugSession) Start(ctx context.Context) error {
242
271
}
243
272
fmt .Fprintf (& b , "Restarted" )
244
273
case line == "c" :
245
- if err := ds .session .Resume ( thr ); err != nil {
274
+ if err := ds .session .ResumeAll ( ); err != nil {
246
275
return fmt .Errorf ("error resuming execution: %w" , err )
247
276
}
248
277
249
- evt := ds .WaitFor (ctx ,
278
+ evt := ds .waitFor (ctx ,
250
279
debug .ExceptionEventType ,
251
280
debug .StoppedEventType ,
252
281
debug .StdoutEventType ,
@@ -280,6 +309,9 @@ func (ds *debugSession) Start(ctx context.Context) error {
280
309
case debug .TerminatedEventType :
281
310
fmt .Fprintf (& b , "\n Terminated\n " )
282
311
}
312
+ case line == "q" :
313
+ return errUserAbort
314
+
283
315
case line == "locals" :
284
316
if err := printLocals (& b , ds .session , thr ); err != nil {
285
317
return fmt .Errorf ("error printing locals: %w" , err )
@@ -343,11 +375,23 @@ func (ds *debugSession) Start(ctx context.Context) error {
343
375
return fmt .Errorf ("error getting stack trace: %w" , err )
344
376
}
345
377
if loc := getCurrentLocation (stack ); loc != nil {
346
- loc .Row += 1 // let's hope it always exists...
347
- loc .Col = 0
378
+ // Unfortunately, getting the column
379
+ // right is tricky, since source-level
380
+ // breakpoints only look at line
381
+ // numbers in the REGO interpreter, so
382
+ // the safest assumption is starting
383
+ // from 0.
384
+ //
385
+ // It would be great if the frame
386
+ // struct contained details about the
387
+ // position in the source.
388
+ nextloc := location.Location {
389
+ Row : loc .Row + 1 , // let's hope it always exists...
390
+ Col : 0 ,
391
+ }
348
392
349
393
// add internal breakpoint
350
- bp , err := ds .session .AddBreakpoint (* loc )
394
+ bp , err := ds .session .AddBreakpoint (nextloc )
351
395
if err != nil {
352
396
return fmt .Errorf ("error setting breakpoint: %w" , err )
353
397
}
@@ -357,7 +401,7 @@ func (ds *debugSession) Start(ctx context.Context) error {
357
401
return fmt .Errorf ("error resuming execution: %w" , err )
358
402
}
359
403
360
- evt := ds .WaitFor (ctx , debug .StoppedEventType )
404
+ evt := ds .waitFor (ctx , debug .StoppedEventType )
361
405
stack , err := ds .session .StackTrace (evt .Thread )
362
406
if err != nil {
363
407
return fmt .Errorf ("error getting stack trace: %w" , err )
@@ -374,43 +418,39 @@ func (ds *debugSession) Start(ctx context.Context) error {
374
418
case line == "s" ,
375
419
line == "sv" :
376
420
if err := ds .session .StepOver (thr ); err != nil {
377
- panic ( err )
421
+ return fmt . Errorf ( "error on step-over: %w" , err )
378
422
}
379
- evt := ds .WaitFor (ctx , debug .StoppedEventType )
423
+ evt := ds .waitFor (ctx , debug .StoppedEventType )
380
424
stack , err := ds .session .StackTrace (evt .Thread )
381
425
if err != nil {
382
426
return fmt .Errorf ("error getting stack trace: %w" , err )
383
427
}
384
428
printSource (& b , ds .src , stack )
385
429
case line == "si" :
386
- go func () {
387
- if err := ds .session .StepIn (thr ); err != nil {
388
- panic (err )
389
- }
390
- }()
391
- evt := ds .WaitFor (ctx , debug .StoppedEventType )
430
+ if err := ds .session .StepIn (thr ); err != nil {
431
+ return fmt .Errorf ("error on step-in: %w" , err )
432
+ }
433
+ evt := ds .waitFor (ctx , debug .StoppedEventType )
392
434
stack , err := ds .session .StackTrace (evt .Thread )
393
435
if err != nil {
394
436
return fmt .Errorf ("error getting stack trace: %w" , err )
395
437
}
396
438
printSource (& b , ds .src , stack )
397
439
case line == "so" :
398
- go func () {
399
- if err := ds .session .StepOut (thr ); err != nil {
400
- panic (err )
401
- }
402
- }()
403
- evt := ds .WaitFor (ctx , debug .StoppedEventType )
440
+ if err := ds .session .StepOut (thr ); err != nil {
441
+ return fmt .Errorf ("error on step-out: %w" , err )
442
+ }
443
+ evt := ds .waitFor (ctx , debug .StoppedEventType )
404
444
stack , err := ds .session .StackTrace (evt .Thread )
405
445
if err != nil {
406
446
return fmt .Errorf ("error getting stack trace: %w" , err )
407
447
}
408
448
printSource (& b , ds .src , stack )
409
- case line == "q" :
410
- return fmt .Errorf ("user abort" )
449
+
411
450
case line == "h" ,
412
451
line == "help" :
413
452
printHelp (& b )
453
+
414
454
case strings .HasPrefix (line , "p" ):
415
455
varname , err := toVarName (line )
416
456
if err != nil {
@@ -433,6 +473,7 @@ func (ds *debugSession) Start(ctx context.Context) error {
433
473
if err := printVar (& b , r , ds .session , thr ); err != nil {
434
474
return fmt .Errorf ("error printing variables: %w" , err )
435
475
}
476
+
436
477
case strings .HasPrefix (line , "b" ):
437
478
loc , err := toLocation (line , ds .lines )
438
479
if err != nil {
@@ -627,10 +668,24 @@ func printSource(b *strings.Builder, src string, stack debug.StackTrace) {
627
668
for idx , line := range strings .Split (src , "\n " ) {
628
669
fmt .Fprintf (b , "%*d: %s" , padding , idx + 1 , line )
629
670
if idx + 1 == loc .Row {
630
- theline := strings .Split (string (loc .Text ), "\n " )[0 ]
671
+ // `theline` is the very first line of
672
+ // the expression starting at the
673
+ // given position.
674
+ //
675
+ // In REGO expressions can span
676
+ // multiple lines (for example, rules
677
+ // do), but we really are interested
678
+ // in underlining only the first line
679
+ // of the given expression.
680
+ //
681
+ // For weird underlyining starting
682
+ // from column 0 of the line, see
683
+ // comment on setting source-level
684
+ // breakpoints.
685
+ theline := strings .Split (line , "\n " )[0 ]
631
686
fmt .Fprintf (b , "\n %s%s" ,
632
- strings .Repeat (" " , loc . Col + int (padding )+ 2 - 1 ),
633
- cli .SimpleBoldStyle .Render (strings .Repeat ("^" , len (theline ))),
687
+ strings .Repeat (" " , int (padding )+ 2 + loc . Col - 1 ),
688
+ cli .SimpleBoldStyle .Render (strings .Repeat ("^" , len (theline )- loc . Col + 1 )),
634
689
)
635
690
}
636
691
fmt .Fprintln (b )
@@ -654,6 +709,7 @@ func printLocals(b *strings.Builder, s debug.Session, thrID debug.ThreadID) erro
654
709
}
655
710
656
711
if len (trace ) == 0 {
712
+ fmt .Fprintln (b , "No locals" )
657
713
return nil
658
714
}
659
715
@@ -843,7 +899,7 @@ Breakpoints:
843
899
clearall/cla -- clear all breakpoints
844
900
845
901
Stepping:
846
- n ---- --------- next line
902
+ n/next --------- next line
847
903
s/sv ---------- step over
848
904
so ------------ step out
849
905
si ------------ step into
0 commit comments