Repository: go-delve/delve
Pull request #3656 (2024-02-06 11:28:05)

service/dap: support waitfor option for 'dap attach' only
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.



Patch:
diff --git a/service/dap/server.go b/service/dap/server.go
index 800639f2..88f4d70b 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/types.go b/service/dap/types.go
index 1ab685f9..59f06065 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
 }
 


Test patch:
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index c2feba4d..f82fc767 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),