service/dap: Support for replay and core modes (#2367)
This PR aims to add support for rr replay and core actions from the DAP layer. This basically encloses the following:
New launch modes: replay and core
The following modes are added:
replay: Replays an rr trace, allowing backwards flows (reverse continue and stepback). Requires a traceDirPath property on launch.json pointing to a valid rr trace directory.
Equivalent to dlv replay <tracedir> command.
core: Replays a core dump file, showing its callstack and the file matching the callsite. Requires a coreFilePath property on launch.json pointing to a valid coredump file.
Equivalent to dlv core <exe> <corefile> command.
Dependencies
To achieve this the following additional changes were made:
Implement the onStepBackRequest and onReverseContinueRequest methods on service/dap
Adapt onLaunchRequest with the requried validations and logic for these new modes
Use CapabilitiesEvent responses to enable the StepBack controls on the supported scenarios (see dicussion here)
Add the corresponding launch.json support on vs code:
Support for replay and core modes golang/vscode-go#1268
This commit is contained in:
parent
a14bec4cfc
commit
69615b3604
@ -12,6 +12,8 @@ to be launched or process to be attached to. The following modes are supported:
|
||||
- launch + exec (executes precompiled binary, like 'dlv exec')
|
||||
- launch + debug (builds and launches, like 'dlv debug')
|
||||
- launch + test (builds and tests, like 'dlv test')
|
||||
- launch + replay (replays an rr trace, like 'dlv replay')
|
||||
- launch + core (replays a core dump file, like 'dlv core')
|
||||
- attach + local (attaches to a running process, like 'dlv attach')
|
||||
The server does not yet accept multiple client connections (--accept-multiclient).
|
||||
While --continue is not supported, stopOnEntry launch/attach attribute can be used to control if
|
||||
|
||||
@ -182,6 +182,8 @@ to be launched or process to be attached to. The following modes are supported:
|
||||
- launch + exec (executes precompiled binary, like 'dlv exec')
|
||||
- launch + debug (builds and launches, like 'dlv debug')
|
||||
- launch + test (builds and tests, like 'dlv test')
|
||||
- launch + replay (replays an rr trace, like 'dlv replay')
|
||||
- launch + core (replays a core dump file, like 'dlv core')
|
||||
- attach + local (attaches to a running process, like 'dlv attach')
|
||||
The server does not yet accept multiple client connections (--accept-multiclient).
|
||||
While --continue is not supported, stopOnEntry launch/attach attribute can be used to control if
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
// stop function which will prematurely terminate the recording of the
|
||||
// program.
|
||||
func RecordAsync(cmd []string, wd string, quiet bool, redirects [3]string) (run func() (string, error), stop func() error, err error) {
|
||||
if err := checkRRAvailabe(); err != nil {
|
||||
if err := checkRRAvailable(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ func Record(cmd []string, wd string, quiet bool, redirects [3]string) (tracedir
|
||||
// Replay starts an instance of rr in replay mode, with the specified trace
|
||||
// directory, and connects to it.
|
||||
func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string) (*proc.Target, error) {
|
||||
if err := checkRRAvailabe(); err != nil {
|
||||
if err := checkRRAvailable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ func (err ErrPerfEventParanoid) Error() string {
|
||||
return fmt.Sprintf("rr needs /proc/sys/kernel/perf_event_paranoid <= 1, but it is %d", err.actual)
|
||||
}
|
||||
|
||||
func checkRRAvailabe() error {
|
||||
func checkRRAvailable() error {
|
||||
if _, err := exec.LookPath("rr"); err != nil {
|
||||
return &ErrBackendUnavailable{}
|
||||
}
|
||||
|
||||
@ -33,11 +33,13 @@ import (
|
||||
"github.com/go-delve/delve/pkg/locspec"
|
||||
"github.com/go-delve/delve/pkg/logflags"
|
||||
"github.com/go-delve/delve/pkg/proc"
|
||||
|
||||
"github.com/go-delve/delve/service"
|
||||
"github.com/go-delve/delve/service/api"
|
||||
"github.com/go-delve/delve/service/debugger"
|
||||
"github.com/go-delve/delve/service/internal/sameuser"
|
||||
"github.com/google/go-dap"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -547,12 +549,18 @@ func (s *Server) handleRequest(request dap.Message) {
|
||||
<-resumeRequestLoop
|
||||
case *dap.StepBackRequest:
|
||||
// Optional (capability ‘supportsStepBack’)
|
||||
// TODO: implement this request in V1
|
||||
s.onStepBackRequest(request)
|
||||
go func() {
|
||||
defer s.recoverPanic(request)
|
||||
s.onStepBackRequest(request, resumeRequestLoop)
|
||||
}()
|
||||
<-resumeRequestLoop
|
||||
case *dap.ReverseContinueRequest:
|
||||
// Optional (capability ‘supportsStepBack’)
|
||||
// TODO: implement this request in V1
|
||||
s.onReverseContinueRequest(request)
|
||||
go func() {
|
||||
defer s.recoverPanic(request)
|
||||
s.onReverseContinueRequest(request, resumeRequestLoop)
|
||||
}()
|
||||
<-resumeRequestLoop
|
||||
//--- Synchronous requests ---
|
||||
case *dap.InitializeRequest:
|
||||
// Required
|
||||
@ -712,7 +720,7 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
|
||||
// TODO(polina): support these requests in addition to vscode-go feature parity
|
||||
response.Body.SupportsTerminateRequest = false
|
||||
response.Body.SupportsRestartRequest = false
|
||||
response.Body.SupportsStepBack = false
|
||||
response.Body.SupportsStepBack = false // To be enabled by CapabilitiesEvent based on configuration
|
||||
response.Body.SupportsSetExpression = false
|
||||
response.Body.SupportsLoadedSourcesRequest = false
|
||||
response.Body.SupportsReadMemoryRequest = false
|
||||
@ -755,13 +763,58 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
||||
|
||||
// TODO(polina): Respond with an error if debug session is in progress?
|
||||
program, ok := request.Arguments["program"].(string)
|
||||
if !ok || program == "" {
|
||||
if (!ok || program == "") && mode != "replay" { // Only fail on modes requiring a program
|
||||
s.sendErrorResponse(request.Request,
|
||||
FailedToLaunch, "Failed to launch",
|
||||
"The program attribute is missing in debug configuration.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if mode == "replay" {
|
||||
traceDirPath, _ := request.Arguments["traceDirPath"].(string)
|
||||
|
||||
|
||||
// Validate trace directory
|
||||
if traceDirPath == "" {
|
||||
s.sendErrorResponse(request.Request,
|
||||
FailedToLaunch, "Failed to launch",
|
||||
"The 'traceDirPath' attribute is missing in debug configuration.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Assign the rr trace directory path to debugger configuration
|
||||
s.config.Debugger.CoreFile = traceDirPath
|
||||
s.config.Debugger.Backend = "rr"
|
||||
}
|
||||
|
||||
if mode == "core" {
|
||||
coreFilePath, _ := request.Arguments["coreFilePath"].(string)
|
||||
|
||||
|
||||
// Validate core dump path
|
||||
if coreFilePath == "" {
|
||||
|
||||
s.sendErrorResponse(request.Request,
|
||||
FailedToLaunch, "Failed to launch",
|
||||
"The 'coreFilePath' attribute is missing in debug configuration.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Assign the non-empty core file path to debugger configuration. This will
|
||||
// trigger a native core file replay instead of an rr trace replay
|
||||
s.config.Debugger.CoreFile = coreFilePath
|
||||
s.config.Debugger.Backend = "core"
|
||||
}
|
||||
|
||||
s.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend)
|
||||
|
||||
// Prepare the debug executable filename, build flags and build it
|
||||
if mode == "debug" || mode == "test" {
|
||||
output, ok := request.Arguments["output"].(string)
|
||||
if !ok || output == "" {
|
||||
@ -786,9 +839,10 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
s.log.Debugf("building binary at %s", debugbinary)
|
||||
var cmd string
|
||||
var out []byte
|
||||
// Log debug binary build
|
||||
s.log.Debugf("building binary '%s' from '%s' with flags '%v'", debugbinary, program, buildFlags)
|
||||
switch mode {
|
||||
case "debug":
|
||||
cmd, out, err = gobuild.GoBuildCombinedOutput(debugbinary, []string{program}, buildFlags)
|
||||
@ -912,6 +966,10 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
||||
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
|
||||
return
|
||||
}
|
||||
// Enable StepBack controls on supported backends
|
||||
if s.config.Debugger.Backend == "rr" {
|
||||
s.send(&dap.CapabilitiesEvent{ Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{ SupportsStepBack: true }}})
|
||||
}
|
||||
|
||||
// Notify the client that the debugger is ready to start accepting
|
||||
// configuration requests for setting breakpoints, etc. The client
|
||||
@ -963,10 +1021,15 @@ func (s *Server) stopNoDebugProcess() {
|
||||
// Required args: program
|
||||
// Optional args with default: cwd, noDebug
|
||||
// Optional args: args
|
||||
// TODO(pull/2367): add "replay", "core"
|
||||
// -- replay: skips program build and sets the Debugger.CoreFile property based on the
|
||||
// Required args: coreFilePath
|
||||
// Optional args: program, args
|
||||
// -- core: skips program build and sets the Debugger.CoreFile property based on the
|
||||
// Required args: program, traceDirPath
|
||||
// Optional args: args
|
||||
func isValidLaunchMode(mode interface{}) bool {
|
||||
switch mode {
|
||||
case "exec", "debug", "test":
|
||||
case "exec", "debug", "test", "replay", "core":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -2407,16 +2470,21 @@ func (s *Server) onRestartRequest(request *dap.RestartRequest) {
|
||||
s.sendNotYetImplementedErrorResponse(request.Request)
|
||||
}
|
||||
|
||||
// onStepBackRequest sends a not-yet-implemented error response.
|
||||
// Capability 'supportsStepBack' is not set 'initialize' response.
|
||||
func (s *Server) onStepBackRequest(request *dap.StepBackRequest) {
|
||||
s.sendNotYetImplementedErrorResponse(request.Request)
|
||||
// onStepBackRequest handles 'stepBack' request.
|
||||
// This is an optional request enabled by capability ‘supportsStepBackRequest’.
|
||||
func (s *Server) onStepBackRequest(request *dap.StepBackRequest, asyncSetupDone chan struct{}) {
|
||||
s.sendStepResponse(request.Arguments.ThreadId, &dap.StepBackResponse{Response: *newResponse(request.Request)})
|
||||
s.doStepCommand(api.ReverseNext, request.Arguments.ThreadId, asyncSetupDone)
|
||||
}
|
||||
|
||||
// onReverseContinueRequest sends a not-yet-implemented error response.
|
||||
// Capability 'supportsStepBack' is not set 'initialize' response.
|
||||
func (s *Server) onReverseContinueRequest(request *dap.ReverseContinueRequest) {
|
||||
s.sendNotYetImplementedErrorResponse(request.Request)
|
||||
// onReverseContinueRequest performs a rewind command call up to the previous
|
||||
// breakpoint or the start of the process
|
||||
// This is an optional request enabled by capability ‘supportsStepBackRequest’.
|
||||
func (s *Server) onReverseContinueRequest(request *dap.ReverseContinueRequest, asyncSetupDone chan struct{}) {
|
||||
s.send(&dap.ReverseContinueResponse{
|
||||
Response: *newResponse(request.Request),
|
||||
})
|
||||
s.doRunCommand(api.Rewind, asyncSetupDone)
|
||||
}
|
||||
|
||||
// computeEvaluateName finds the named child, and computes its evaluate name.
|
||||
@ -2855,4 +2923,4 @@ func (s *Server) toServerPath(path string) string {
|
||||
s.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
|
||||
}
|
||||
return serverPath
|
||||
}
|
||||
}
|
||||
@ -4571,11 +4571,6 @@ func TestOptionalNotYetImplementedResponses(t *testing.T) {
|
||||
client.RestartRequest()
|
||||
expectNotYetImplemented("restart")
|
||||
|
||||
client.StepBackRequest()
|
||||
expectNotYetImplemented("stepBack")
|
||||
|
||||
client.ReverseContinueRequest()
|
||||
expectNotYetImplemented("reverseContinue")
|
||||
|
||||
client.SetExpressionRequest()
|
||||
expectNotYetImplemented("setExpression")
|
||||
@ -4752,6 +4747,20 @@ func TestBadLaunchRequests(t *testing.T) {
|
||||
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "noDebug": true, "cwd": "dir/invalid"})
|
||||
checkFailedToLaunch(client.ExpectErrorResponse(t)) // invalid directory, the error message is system-dependent.
|
||||
|
||||
// Bad parameters on "replay" and "core" modes
|
||||
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "replay", "traceDirPath": ""})
|
||||
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
|
||||
"Failed to launch: The 'traceDirPath' attribute is missing in debug configuration.")
|
||||
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "core", "coreFilePath": ""})
|
||||
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
|
||||
"Failed to launch: The program attribute is missing in debug configuration.")
|
||||
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "replay", "program": fixture.Source, "traceDirPath": ""})
|
||||
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
|
||||
"Failed to launch: The 'traceDirPath' attribute is missing in debug configuration.")
|
||||
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "core", "program": fixture.Source, "coreFilePath": ""})
|
||||
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
|
||||
"Failed to launch: The 'coreFilePath' attribute is missing in debug configuration.")
|
||||
|
||||
// We failed to launch the program. Make sure shutdown still works.
|
||||
client.DisconnectRequest()
|
||||
dresp := client.ExpectDisconnectResponse(t)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user