================
@@ -0,0 +1,813 @@
+# FIXME: remove when LLDB_MINIMUM_PYTHON_VERSION > 3.8
+from __future__ import annotations
+
+import bisect
+import contextlib
+import itertools
+import json
+import os
+import socket
+import subprocess
+import threading
+import time
+from concurrent.futures import Future
+from dataclasses import asdict, dataclass, field, replace
+from pprint import pformat
+from typing import IO, Callable, Optional, Protocol, Tuple, Type, 
runtime_checkable
+
+from .dap_types import (
+    AnyEvent,
+    DAPError,
+    Event,
+    MessageType,
+    RawMessage,
+    Request,
+    Response,
+)
+
+
+# See lldbtest.Base.spawnSubprocess, which should help ensure any processes
+# created by the DAP client are terminated correctly when the test ends.
+class SubProcessSpawner(Protocol):
+    def __call__(
+        self,
+        executable: str,
+        args: list[str] | None = None,
+        extra_env: list[str] | None = None,
+        install_remote: bool = True,
+        **kwargs,
+    ) -> subprocess.Popen[bytes]:
+        ...
+
+
+@dataclass(frozen=True)
+class DebugAdapterOptions:
+    """The options passed when spawning the debug adapter."""
+
+    args: list[str] = field(default_factory=list)
+    env: dict[str, str] = field(default_factory=dict)
+    cwd: Optional[str] = None
+    pre_init_commands: Optional[list[str]] = None
+    log_file: Optional[str] = None
+    # sever_mode related options.
+    connection: Optional[str] = None
+    connection_timeout: Optional[int] = None
+
+    @property
+    def run_as_server(self):
+        return self.connection is not None
+
+    def clone(self, **kwargs) -> DebugAdapterOptions:
+        """Returns a copy with the given fields overridden."""
+        return replace(self, **kwargs)
+
+    def __repr__(self):
+        return f"{type(self).__name__}: {pformat(asdict(self), indent=2, 
compact=True)}"
+
+    def __post_init__(self):
+        # Check connection options is not in args.
+        if "--connection" in self.args or "--connection-timeout" in self.args:
+            raise DAPError(
+                f"--connection in adapter options, use the connection field 
instead {self}"
+            )
+
+        if not self.run_as_server and self.connection_timeout is not None:
+            raise DAPError(
+                f"'--connection-timeout' option can only be used when a 
connection is specified: {self}"
+            )
+
+
+class DebugAdapter:
+    """Spawns and owns the lifetime of lldb-dap binary"""
+
+    _listening_uri: Optional[str]
+
+    def __init__(self, executable: str, opts: DebugAdapterOptions):
+        self.executable = executable
+        self._connection_count = 0
+        self._is_server = opts.run_as_server
+
+        # Setup the process args.
+        process_args = [self.executable]
+        process_args.extend(opts.args)
+
+        if pre_init_commands := opts.pre_init_commands:
+            for command in pre_init_commands:
+                process_args.extend(["--pre-init-command", command])
+
+        # Verify we are using the correct args in stdio or server mode.
+        if opts.run_as_server:
+            process_args.extend(["--connection", opts.connection])  # type: 
ignore
+            if opts.connection_timeout:
+                connection_timeout = str(opts.connection_timeout)
+                process_args.extend(["--connection-timeout", 
connection_timeout])
+
+        # Setup process environment.
+        process_env = os.environ.copy()
+        process_env.update(opts.env)
+        if log_file := opts.log_file:
+            process_env["LLDBDAP_LOG"] = log_file
+
+        self._process = subprocess.Popen(
+            process_args,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            env=process_env,
+            cwd=opts.cwd,
+        )
+        assert self.is_alive, "expected running process"
+
+        if self.is_server:
+            self._listening_uri = self._read_listening_uri()
+        else:
+            self._listening_uri = None
+
+    def create_connection(self) -> DAPConnection:
+        if self.is_server:
+            assert self._listening_uri is not None
+            transport = _SocketTransport(uri=self._listening_uri)
+        else:
+            if self._connection_count > 0:
+                raise DAPError("Cannot create multiple connections in stdio 
mode")
+            transport = _StdioTransport(self._process)
+
+        count = self._connection_count
+        connection_id = f"conn{count}" if self.is_server else "stdio"
+        self._connection_count += 1
+        return DAPConnection(connection_id, transport)
+
+    @property
+    def is_server(self):
+        return self._is_server
+
+    @property
+    def is_alive(self):
+        return self._process.poll() is None
+
+    @property
+    def process(self):
+        return self._process
+
+    def kill(self):
+        self._process.terminate()
+        try:
+            self._process.wait(timeout=2.0)
+        except subprocess.TimeoutExpired:
+            self._process.kill()
+
+    def _read_listening_uri(self) -> str:
+        # lldb-dap will print the listening address once the listener is
+        # made to stdout. The listener is formatted like
+        # `connection://host:port` or `unix-connection:///path`.
+        expected_prefix = "Listening for: "
+        process_stdout = self._process.stdout
+        if process_stdout is None:
+            raise AttributeError("expected the process stdout to be a PIPE")
+
+        out = process_stdout.readline().decode()
----------------
da-viper wrote:

This is fine, since it is less likely to happen. If it does will timeout loudly 
and affect the entire dap test. 

https://github.com/llvm/llvm-project/pull/203978
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to