Repository: go-delve/delve (go)

Issue:
Feature request: Make it possible to listen on a Unix Domain Socket
At the moment, it is only possible to listen to a TCP port.
However, it would be very handy if a domain socket could be used instead. 
Especially in multi-service situations, it would make many things easier. 

For example, if you run multiple services in the headless mode, you must know many ports without any semantics to it. 
If it would be possible to give a Unix Domain Socket, instead, you could have the socket named by the service name. Something like `/tmp/service_x_debug.sock` or so.

Unix Domain Sockets are easy to use in Go.  In the end, you must change:
```
listener, err = net.Listen("tcp", addr)
```
to
```
listener, err = net.Listen("unix", path)
```

I would propose:
1.  a new command line option like `--unix` could be used as an alternative to  `--listen` 
or
2.  the possibility of giving `--listen` a connection string-like format, something like `unix:path`.


Pull request:
cmd/dlv: support unix domain sockets
Add support for listening on a unix domain socket in headless mode and
in the 'connect' command.

Fixes #3654


Patch:
diff --git a/Documentation/usage/dlv_attach.md b/Documentation/usage/dlv_attach.md
index a86973cd..58e4ae9d 100644
--- a/Documentation/usage/dlv_attach.md
+++ b/Documentation/usage/dlv_attach.md
@@ -35,7 +35,7 @@ dlv attach pid [executable] [flags]
       --check-go-version                 Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve. (default true)
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_backend.md b/Documentation/usage/dlv_backend.md
index 3ccc9ba9..d4c2bcbb 100644
--- a/Documentation/usage/dlv_backend.md
+++ b/Documentation/usage/dlv_backend.md
@@ -32,7 +32,7 @@ are:
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_connect.md b/Documentation/usage/dlv_connect.md
index c83e94d0..ea06d67e 100644
--- a/Documentation/usage/dlv_connect.md
+++ b/Documentation/usage/dlv_connect.md
@@ -4,7 +4,7 @@ Connect to a headless debug server with a terminal client.
 
 ### Synopsis
 
-Connect to a running headless debug server with a terminal client.
+Connect to a running headless debug server with a terminal client. Prefix with 'unix:' to use a unix domain socket.
 
 ```
 dlv connect addr [flags]
diff --git a/Documentation/usage/dlv_core.md b/Documentation/usage/dlv_core.md
index 456e908c..d582cc1f 100644
--- a/Documentation/usage/dlv_core.md
+++ b/Documentation/usage/dlv_core.md
@@ -31,7 +31,7 @@ dlv core <executable> <core> [flags]
       --check-go-version                 Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve. (default true)
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_dap.md b/Documentation/usage/dlv_dap.md
index 3f5f7db8..fafb1e7d 100644
--- a/Documentation/usage/dlv_dap.md
+++ b/Documentation/usage/dlv_dap.md
@@ -42,7 +42,7 @@ dlv dap [flags]
 ```
       --check-go-version    Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve. (default true)
       --disable-aslr        Disables address space randomization
-  -l, --listen string       Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string       Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                 Enable debugging server logging.
       --log-dest string     Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string   Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_debug.md b/Documentation/usage/dlv_debug.md
index 23c75ea8..572b65f6 100644
--- a/Documentation/usage/dlv_debug.md
+++ b/Documentation/usage/dlv_debug.md
@@ -36,7 +36,7 @@ dlv debug [package] [flags]
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_exec.md b/Documentation/usage/dlv_exec.md
index 70d96536..10099651 100644
--- a/Documentation/usage/dlv_exec.md
+++ b/Documentation/usage/dlv_exec.md
@@ -35,7 +35,7 @@ dlv exec <path/to/binary> [flags]
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_log.md b/Documentation/usage/dlv_log.md
index 7dc6ed2c..b1d4eab5 100644
--- a/Documentation/usage/dlv_log.md
+++ b/Documentation/usage/dlv_log.md
@@ -48,7 +48,7 @@ and dap modes.
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_redirect.md b/Documentation/usage/dlv_redirect.md
index 940b1359..f4bb0689 100644
--- a/Documentation/usage/dlv_redirect.md
+++ b/Documentation/usage/dlv_redirect.md
@@ -35,7 +35,7 @@ File redirects can also be changed using the 'restart' command.
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_replay.md b/Documentation/usage/dlv_replay.md
index 6f783563..a080f586 100644
--- a/Documentation/usage/dlv_replay.md
+++ b/Documentation/usage/dlv_replay.md
@@ -30,7 +30,7 @@ dlv replay [trace directory] [flags]
       --check-go-version                 Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve. (default true)
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_run.md b/Documentation/usage/dlv_run.md
index 6ab4b674..02b7fe73 100644
--- a/Documentation/usage/dlv_run.md
+++ b/Documentation/usage/dlv_run.md
@@ -24,7 +24,7 @@ dlv run [flags]
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/Documentation/usage/dlv_version.md b/Documentation/usage/dlv_version.md
index b04dd76e..92f26b8b 100644
--- a/Documentation/usage/dlv_version.md
+++ b/Documentation/usage/dlv_version.md
@@ -24,7 +24,7 @@ dlv version [flags]
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go
index 2cda26e0..63b2b118 100644
--- a/cmd/dlv/cmds/commands.go
+++ b/cmd/dlv/cmds/commands.go
@@ -137,7 +137,7 @@ func New(docCall bool) *cobra.Command {
 		Long:  dlvCommandLongDesc,
 	}
 
-	rootCommand.PersistentFlags().StringVarP(&addr, "listen", "l", "127.0.0.1:0", "Debugging server listen address.")
+	rootCommand.PersistentFlags().StringVarP(&addr, "listen", "l", "127.0.0.1:0", "Debugging server listen address. Prefix with 'unix:' to use a unix domain socket.")
 
 	rootCommand.PersistentFlags().BoolVarP(&log, "log", "", false, "Enable debugging server logging.")
 	rootCommand.PersistentFlags().StringVarP(&logOutput, "log-output", "", "", `Comma separated list of components that should produce debug output (see 'dlv help log')`)
@@ -184,7 +184,7 @@ option to let the process continue or kill it.
 	connectCommand := &cobra.Command{
 		Use:   "connect addr",
 		Short: "Connect to a headless debug server with a terminal client.",
-		Long:  "Connect to a running headless debug server with a terminal client.",
+		Long:  "Connect to a running headless debug server with a terminal client. Prefix with 'unix:' to use a unix domain socket.",
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) == 0 {
 				return errors.New("you must provide an address as the first argument")
@@ -514,7 +514,7 @@ func dapCmd(cmd *cobra.Command, args []string) {
 		}
 		var conn net.Conn
 		if dapClientAddr == "" {
-			listener, err := net.Listen("tcp", addr)
+			listener, err := netListen(addr)
 			if err != nil {
 				fmt.Printf("couldn't start listener: %s\n", err)
 				return 1
@@ -912,11 +912,10 @@ func splitArgs(cmd *cobra.Command, args []string) ([]string, []string) {
 func connect(addr string, clientConn net.Conn, conf *config.Config) int {
 	// Create and start a terminal - attach to running instance
 	var client *rpc2.RPCClient
-	if clientConn != nil {
-		client = rpc2.NewClientFromConn(clientConn)
-	} else {
-		client = rpc2.NewClient(addr)
+	if clientConn == nil {
+		clientConn = netDial(addr)
 	}
+	client = rpc2.NewClientFromConn(clientConn)
 	if client.IsMulticlient() {
 		state, _ := client.GetStateNonBlocking()
 		// The error return of GetState will usually be the ErrProcessExited,
@@ -1002,7 +1001,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
 
 	// Make a TCP listener
 	if headless {
-		listener, err = net.Listen("tcp", addr)
+		listener, err = netListen(addr)
 	} else {
 		listener, clientConn = service.ListenerPipe()
 	}
@@ -1128,3 +1127,26 @@ func configUsageFunc(cmd *cobra.Command) {
 		return usage(cmd)
 	})
 }
+
+const unixAddrPrefix = "unix:"
+
+func netListen(addr string) (net.Listener, error) {
+	if strings.HasPrefix(addr, unixAddrPrefix) {
+		return net.Listen("unix", addr[len(unixAddrPrefix):])
+	}
+	return net.Listen("tcp", addr)
+}
+
+func netDial(addr string) net.Conn {
+	var conn net.Conn
+	var err error
+	if strings.HasPrefix(addr, unixAddrPrefix) {
+		conn, err = net.Dial("unix", addr[len(unixAddrPrefix):])
+	} else {
+		conn, err = net.Dial("tcp", addr)
+	}
+	if err != nil {
+		logflags.RPCLogger().Errorf("error dialing %s: %v", addr, err)
+	}
+	return conn
+}
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/Documentation/usage/dlv_test.md b/Documentation/usage/dlv_test.md
index 37a9fe0f..d849d1da 100644
--- a/Documentation/usage/dlv_test.md
+++ b/Documentation/usage/dlv_test.md
@@ -38,7 +38,7 @@ dlv test [package] [flags]
       --disable-aslr                     Disables address space randomization
       --headless                         Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections.
       --init string                      Init file, executed by the terminal client.
-  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
+  -l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
       --log                              Enable debugging server logging.
       --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
       --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 2b057c22..c4e903ca 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -1359,3 +1359,65 @@ func TestDefaultBinary(t *testing.T) {
 		t.Errorf("outputs match")
 	}
 }
+
+func TestUnixDomainSocket(t *testing.T) {
+	tmpdir := os.TempDir()
+	if tmpdir == "" {
+		return
+	}
+
+	listenPath := filepath.Join(tmpdir, "delve_test")
+
+	var err error
+
+	cmd := exec.Command("go", "run", "_scripts/make.go", "build")
+	cmd.Dir = projectRoot()
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("makefile error: %v\noutput %s\n", err, string(out))
+	}
+
+	dlvbin := filepath.Join(cmd.Dir, "dlv")
+	defer os.Remove(dlvbin)
+
+	fixtures := protest.FindFixturesDir()
+
+	buildtestdir := filepath.Join(fixtures, "buildtest")
+
+	cmd = exec.Command(dlvbin, "debug", "--headless=true", "--listen=unix:"+listenPath, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc")
+	cmd.Dir = buildtestdir
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+
+	assertNoError(cmd.Start(), t, "dlv debug")
+
+	scan := bufio.NewScanner(stderr)
+	// wait for the debugger to start
+	for scan.Scan() {
+		text := scan.Text()
+		t.Log(text)
+		if strings.Contains(text, "API server pid = ") {
+			break
+		}
+	}
+	go func() {
+		for scan.Scan() {
+			t.Log(scan.Text())
+			// keep pipe empty
+		}
+	}()
+
+	conn, err := net.Dial("unix", listenPath)
+	assertNoError(err, t, "dialing")
+
+	client := rpc2.NewClientFromConn(conn)
+	state := <-client.Continue()
+
+	if !state.Exited {
+		t.Fatal("Program did not exit")
+	}
+
+	client.Detach(true)
+	cmd.Wait()
+}
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),