From f2b3b18ca714eb096433772a5557a086b21b6e2b Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Wed, 7 Feb 2024 20:03:36 +0100 Subject: [PATCH] service/dap: support waitfor option for 'dap attach' only (#3656) Add a waitfor option for 'dap attach' that waits for a process with a given name to appear before attaching to it. This recovers PR #3584, originally by @muggle-nil, which was fine except for a broken test. --- service/dap/server.go | 26 +++++++++++++---- service/dap/server_test.go | 59 ++++++++++++++++++++++++++++++++++---- service/dap/types.go | 6 +++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/service/dap/server.go b/service/dap/server.go index 800639f289..88f4d70b02 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1892,17 +1892,33 @@ func (s *Session) onAttachRequest(request *dap.AttachRequest) { fmt.Sprintf("debug session already in progress at %s - use remote mode to connect to a server with an active debug session", s.address())) return } - if args.ProcessID == 0 { - s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", - "The 'processId' attribute is missing in debug configuration") + if args.AttachWaitFor != "" && args.ProcessID != 0 { + s.sendShowUserErrorResponse( + request.Request, + FailedToAttach, + "Failed to attach", + "'processId' and 'waitFor' are mutually exclusive, and can't be specified at the same time") + return + } else if args.AttachWaitFor != "" { + s.config.Debugger.AttachWaitFor = args.AttachWaitFor + // Keep the default value the same as waitfor-interval. + s.config.Debugger.AttachWaitForInterval = 1 + s.config.log.Debugf("Waiting for a process with a name beginning with this prefix: %s", args.AttachWaitFor) + } else if args.ProcessID != 0 { + s.config.Debugger.AttachPid = args.ProcessID + s.config.log.Debugf("Attaching to pid %d", args.ProcessID) + } else { + s.sendShowUserErrorResponse( + request.Request, + FailedToAttach, + "Failed to attach", + "The 'processId' or 'waitFor' attribute is missing in debug configuration") return } - s.config.Debugger.AttachPid = args.ProcessID if args.Backend == "" { args.Backend = "default" } s.config.Debugger.Backend = args.Backend - s.config.log.Debugf("attaching to pid %d", args.ProcessID) var err error func() { s.mu.Lock() diff --git a/service/dap/server_test.go b/service/dap/server_test.go index c2feba4dcb..f82fc7673c 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -5901,6 +5901,51 @@ func TestAttachRequest(t *testing.T) { }) } +func TestAttachWaitForRequest(t *testing.T) { + if runtime.GOOS == "freebsd" { + // The value of /proc/[pid]/cmdline might be wrong on FreeBSD: zero or the same as the parent process. + t.Skip("test skipped on freebsd") + } + runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) { + cmd := exec.Command(fixture.Path) + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + // Wait for output. + // This will give the target process time to initialize the runtime before we attach, + // so we can rely on having goroutines when they are requested on attach. + scanOut := bufio.NewScanner(stdout) + scanOut.Scan() + if scanOut.Text() != "past main" { + t.Errorf("expected loopprog.go to output \"past main\"") + } + + t.Logf("The process id of program %q is %d", cmd.Path, cmd.Process.Pid) + + client.InitializeRequest() + client.ExpectInitializeResponseAndCapabilities(t) + client.AttachRequest(map[string]interface{}{ + "mode": "local", + "waitFor": fixture.Path, + "stopOnEntry": true, + }) + client.ExpectCapabilitiesEventSupportTerminateDebuggee(t) + client.ExpectInitializedEvent(t) + client.ExpectAttachResponse(t) + client.DisconnectRequestWithKillOption(true) + client.ExpectOutputEvent(t) + client.ExpectDisconnectResponse(t) + client.ExpectTerminatedEvent(t) + + runtime.KeepAlive(stdout) + }) +} + // Since we are in async mode while running, we might receive thee messages after pause request // in either order. func expectPauseResponseAndStoppedEvent(t *testing.T, client *daptest.Client) { @@ -6621,24 +6666,28 @@ func TestBadAttachRequest(t *testing.T) { client.AttachRequest(map[string]interface{}{"mode": ""}) // empty mode defaults to "local" (not an error) checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), - "Failed to attach: The 'processId' attribute is missing in debug configuration") + "Failed to attach: The 'processId' or 'waitFor' attribute is missing in debug configuration") client.AttachRequest(map[string]interface{}{}) // no mode defaults to "local" (not an error) checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), - "Failed to attach: The 'processId' attribute is missing in debug configuration") + "Failed to attach: The 'processId' or 'waitFor' attribute is missing in debug configuration") // Bad "processId" client.AttachRequest(map[string]interface{}{"mode": "local"}) checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), - "Failed to attach: The 'processId' attribute is missing in debug configuration") + "Failed to attach: The 'processId' or 'waitFor' attribute is missing in debug configuration") client.AttachRequest(map[string]interface{}{"mode": "local", "processId": nil}) checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), - "Failed to attach: The 'processId' attribute is missing in debug configuration") + "Failed to attach: The 'processId' or 'waitFor' attribute is missing in debug configuration") client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 0}) checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), - "Failed to attach: The 'processId' attribute is missing in debug configuration") + "Failed to attach: The 'processId' or 'waitFor' attribute is missing in debug configuration") + + client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 1, "waitFor": "loopprog"}) + checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), + "Failed to attach: 'processId' and 'waitFor' are mutually exclusive, and can't be specified at the same time") client.AttachRequest(map[string]interface{}{"mode": "local", "processId": "1"}) checkFailedToAttachWithMessage(client.ExpectVisibleErrorResponse(t), diff --git a/service/dap/types.go b/service/dap/types.go index 1ab685f965..59f06065ab 100644 --- a/service/dap/types.go +++ b/service/dap/types.go @@ -237,6 +237,7 @@ func (m *SubstitutePath) UnmarshalJSON(data []byte) error { } // AttachConfig is the collection of attach request attributes recognized by DAP implementation. +// 'processId' and 'waitFor' are mutually exclusive, and can't be specified at the same time. type AttachConfig struct { // Acceptable values are: // "local": attaches to the local process with the given ProcessID. @@ -245,9 +246,12 @@ type AttachConfig struct { // Default is "local". Mode string `json:"mode"` - // The numeric ID of the process to be debugged. Required and must not be 0. + // The numeric ID of the process to be debugged. ProcessID int `json:"processId,omitempty"` + // Wait for a process with a name beginning with this prefix. + AttachWaitFor string `json:"waitFor,omitempty"` + LaunchAttachCommonConfig }