Skip to content

Commit a2aeea0

Browse files
committed
Support "LastValue" requests to rewind to most recent writes.
1 parent 10259e3 commit a2aeea0

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

service/dap/server.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,12 @@ func (s *Session) handleRequest(request dap.Message) {
854854
s.onGotoCheckpointRequest(request, resumeRequestLoop)
855855
}()
856856
resumeRequestLoop.wait()
857+
case *LastValueRequest: // Custom (Undo backend)
858+
go func() {
859+
defer s.recoverPanic(request)
860+
s.onLastValueRequest(request, resumeRequestLoop)
861+
}()
862+
resumeRequestLoop.wait()
857863
case *dap.ReverseContinueRequest: // Optional (capability 'supportsStepBack')
858864
go func() {
859865
defer s.recoverPanic(request)
@@ -3520,6 +3526,119 @@ func (s *Session) onListCheckpointsRequest(request *ListCheckpointsRequest) {
35203526
})
35213527
}
35223528

3529+
func (s *Session) rewindWithWatchpoint(thread int64, frame int, expression string, allowNextStateChange *syncflag) (*api.DebuggerState, bool, error) {
3530+
wp, err := s.debugger.CreateWatchpoint(thread, frame, 0, expression, api.WatchWrite)
3531+
if err != nil {
3532+
return nil, false, err
3533+
}
3534+
3535+
// We tell the client that the debuggee has been "continued" so it will expect a stop event.
3536+
s.send(&dap.ContinuedEvent{
3537+
Event: *newEvent("continued"),
3538+
Body: dap.ContinuedEventBody{
3539+
ThreadId: int(thread),
3540+
AllThreadsContinued: true,
3541+
},
3542+
})
3543+
3544+
// Attempt to rewind.
3545+
state, err := s.runUntilStop(api.Rewind, allowNextStateChange)
3546+
3547+
// Clear up our watchpoint - we definitely don't need it anymore.
3548+
_, wp_err := s.debugger.ClearBreakpoint(wp)
3549+
3550+
err = errors.Join(err, wp_err)
3551+
if err != nil {
3552+
return nil, false, err
3553+
}
3554+
3555+
return state, s.debugger.StopReason() == proc.StopWatchpoint, nil
3556+
}
3557+
3558+
// onLastValueRequest handles the 'undo/lastValue' request.
3559+
// This is a custom request supported by Undo's Delve fork.
3560+
func (s *Session) onLastValueRequest(request *LastValueRequest, allowNextStateChange *syncflag) {
3561+
defer allowNextStateChange.raise()
3562+
3563+
breakpoints := s.debugger.Breakpoints(true)
3564+
defer func() {
3565+
// On exit, restore the original status of all breakpoints.
3566+
for _, bp := range breakpoints {
3567+
s.debugger.AmendBreakpoint(bp)
3568+
}
3569+
}()
3570+
3571+
sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId)
3572+
// Todo: Does sf.goroutineID mean we don't need the ThreadID argument?
3573+
if !ok {
3574+
s.sendErrorResponse(
3575+
request.Request,
3576+
UnableToSetBreakpoints,
3577+
"Unable to set watchpoint",
3578+
fmt.Sprintf("unknown frame id %d", request.Arguments.FrameId),
3579+
)
3580+
return
3581+
}
3582+
3583+
// Disable all breakpoints to avoid confusion with the watchpoint we'll create.
3584+
for _, bp := range breakpoints {
3585+
bp_disabled := *bp
3586+
bp_disabled.Disabled = true
3587+
s.debugger.AmendBreakpoint(&bp_disabled)
3588+
}
3589+
3590+
state_before, err := s.debugger.State(false)
3591+
if err != nil {
3592+
s.sendInternalErrorResponse(request.Seq, err.Error())
3593+
}
3594+
3595+
state, found, err := s.rewindWithWatchpoint(
3596+
int64(request.Arguments.ThreadId),
3597+
sf.frameIndex,
3598+
request.Arguments.Expression,
3599+
allowNextStateChange,
3600+
)
3601+
if err != nil {
3602+
s.sendInternalErrorResponse(request.Seq, err.Error())
3603+
return
3604+
}
3605+
3606+
// If we didn't find a value change then return to the initial point in time.
3607+
// Todo:
3608+
// - probably need to restore current thread, frame, etc also.
3609+
// - if we were already at the start of time a strange error comes out - investigate!
3610+
if !found {
3611+
_, err := s.debugger.Restart(false, state_before.When, false, nil, [3]string{}, false)
3612+
if err != nil {
3613+
s.sendInternalErrorResponse(request.Seq, err.Error())
3614+
}
3615+
}
3616+
3617+
s.send(&LastValueResponse{
3618+
Response: *newResponse(request.Request),
3619+
Body: LastValueResult{Found: found},
3620+
})
3621+
3622+
// We attempt to roughly match behaviour the behaviour of stopping after a step / continue
3623+
// here, since there's no stop reason for an explicit time jump.
3624+
s.resetHandlesForStoppedEvent()
3625+
stopped := dap.StoppedEvent{
3626+
Event: *newEvent("stopped"),
3627+
Body: dap.StoppedEventBody{
3628+
ThreadId: int(stoppedGoroutineID(state)),
3629+
AllThreadsStopped: true,
3630+
},
3631+
}
3632+
3633+
if found {
3634+
stopped.Body.Reason = "data breakpoint"
3635+
} else {
3636+
stopped.Body.Reason = "step"
3637+
}
3638+
3639+
s.send(&stopped)
3640+
}
3641+
35233642
// onReverseContinueRequest performs a rewind command call up to the previous
35243643
// breakpoint or the start of the process
35253644
// This is an optional request enabled by capability 'supportsStepBackRequest'.

service/dap/undo_dap.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ type DeleteCheckpointRequest struct {
5252
Arguments CheckpointArgs
5353
}
5454

55+
type LastValueRequest struct {
56+
dap.Request
57+
58+
Arguments struct {
59+
Expression string `json:"expression"`
60+
FrameId int `json:"frameId"`
61+
ThreadId int `json:"threadId"`
62+
}
63+
}
64+
5565
type StepOverBackResponse struct {
5666
dap.Response
5767
}
@@ -102,6 +112,16 @@ type ListCheckpointsResponse struct {
102112
Body ListCheckpointsBody `json:"body"`
103113
}
104114

115+
type LastValueResult struct {
116+
Found bool
117+
}
118+
119+
type LastValueResponse struct {
120+
dap.Response
121+
122+
Body LastValueResult `json:"body"`
123+
}
124+
105125
func makeUndoDapCodec() *dap.Codec {
106126
codec := dap.NewCodec()
107127
codec.RegisterRequest("undo/stepOverBack",
@@ -144,5 +164,10 @@ func makeUndoDapCodec() *dap.Codec {
144164
func() dap.Message { return &ListCheckpointsResponse{} },
145165
)
146166

167+
codec.RegisterRequest("undo/lastValue",
168+
func() dap.Message { return &LastValueRequest{} },
169+
func() dap.Message { return &LastValueResponse{} },
170+
)
171+
147172
return codec
148173
}

0 commit comments

Comments
 (0)