Providing a Custom Transport - microsoft/VSDebugAdapterHost GitHub Wiki

Overview

The Visual Studio Debug Adapter Host communicates with debug adapters using the Debug Adapter Protocol (DAP). By default, this communication happens over standard input/output streams of a locally launched debug adapter process. However, some scenarios require a custom transport — for example, communicating with a debug adapter over a network connection, through a named pipe, or via an intermediary process.

The Debug Adapter Host provides the IAdapterLauncher extensibility interface to support these scenarios. By implementing IAdapterLauncher, a Visual Studio extension can take full control of how the debug adapter process is started and how the DAP message streams are established.

Prerequisites

Install the Microsoft.VisualStudio.Debugger.DebugAdapterHost.Interfaces NuGet package, which contains all the public interfaces described in this document:

https://www.nuget.org/packages/Microsoft.VisualStudio.Debugger.DebugAdapterHost.Interfaces

Key Interfaces

IAdapterLauncher

This is the primary interface you implement to provide a custom transport. It derives from IDebugAdapterHostComponent.

public interface IAdapterLauncher : IDebugAdapterHostComponent
{
    /// Called before launching the debug adapter, allowing you to inspect or modify
    /// the launch configuration.
    void UpdateLaunchOptions(IAdapterLaunchInfo launchInfo);

    /// Called to launch the debug adapter. Return an ITargetHostProcess whose
    /// StandardInput and StandardOutput streams will be used for DAP communication.
    /// Return null to fall back to the default launch behavior.
    ITargetHostProcess LaunchAdapter(IAdapterLaunchInfo launchInfo, ITargetHostInterop targetInterop);
}

IDebugAdapterHostComponent

Base interface for all Debug Adapter Host extensibility components. Your implementation will receive a context object during initialization that provides access to logging and session events.

public interface IDebugAdapterHostComponent
{
    /// Called to initialize the component with a per-session context.
    void Initialize(IDebugAdapterHostContext context);
}

IDebugAdapterHostContext

Provided to your component during Initialize. Use it to access logging and events.

public interface IDebugAdapterHostContext
{
    IDebugAdapterHostLogger Logger { get; }
    IDebugAdapterHostEvents Events { get; }
    T GetUserData<T>();
    void SetUserData<T>(T userData);
}

ITargetHostProcess

Represents the debug adapter process (or a virtual process wrapping your custom transport). The Debug Adapter Host reads DAP messages from StandardOutput and writes them to StandardInput. This is the central abstraction that connects your custom transport to the Debug Adapter Host.

public interface ITargetHostProcess
{
    /// Process handle for local processes. Set to IntPtr.Zero if not applicable.
    IntPtr Handle { get; }

    /// Stream the Debug Adapter Host writes DAP messages to (adapter input).
    Stream StandardInput { get; }

    /// Stream the Debug Adapter Host reads DAP messages from (adapter output).
    Stream StandardOutput { get; }

    /// Terminates the adapter process or closes the transport.
    void Terminate();

    /// Indicates whether the process/transport has exited.
    bool HasExited { get; }

    /// Raised when the process/transport exits.
    event EventHandler Exited;

    /// Raised when error output is produced.
    event DataReceivedEventHandler ErrorDataReceived;
}

IAdapterLaunchInfo

Contains information about the debug session being launched. You can read properties to determine the launch mode and modify the LaunchJson to control what configuration is sent to the debug adapter.

public interface IAdapterLaunchInfo
{
    /// JSON launch configuration that will be provided to the debug adapter.
    /// Can be modified in UpdateLaunchOptions.
    string LaunchJson { get; set; }

    /// Whether this is a Launch, Attach, or NonDebugLaunch session.
    LaunchType LaunchType { get; }

    /// Whether the adapter should be launched locally or on a remote UNIX host
    /// (via one of the built-in UNIX transports: SSH, Docker Linux, or WSL).
    LaunchLocation LaunchLocation { get; }

    /// When attaching, the process ID of the target process.
    int AttachProcessId { get; }

    /// The AD7 IDebugPort2 used for this debug connection.
    IDebugPort2 DebugPort { get; }

    /// Returns the value of a metric from the debug engine registration.
    string GetMetricString(string metricName);
}

ITargetHostInterop

Provides helper methods for interacting with the target host (local or remote). Your launcher can use these methods to execute processes and manage files.

Note: Currently, ITargetHostInterop operates in one of two modes. If the Debug Adapter Host is launched through one of the built-in UNIX transports (SSH, Docker Linux, or WSL), this interface provides methods to interact with those remote systems. Otherwise, it provides an interface for interacting with the local computer. If you are providing a custom transport to a system other than these, you should not consume this interface — instead, manage your connection and process lifecycle directly within your IAdapterLauncher implementation and return an ITargetHostProcess that wraps your own streams.

public interface ITargetHostInterop
{
    string HostIdentifier { get; }
    TargetPlatform TargetPlatform { get; }

    string ExecuteCommand(string description, string command, string arguments,
                          int timeout, out int exitCode);
    ITargetHostProcess ExecuteCommandAsync(string command, string arguments);
    ITargetHostProcess ExecuteCommandAsync(string runtimeId, string command, string arguments);
    ITargetHostProcess ExecuteCommandAsync(string runtimeId, string command, string arguments,
                                           Dictionary<string, string> environment);

    void CopyFile(string localPath, string targetPath);
    string CreateDirectory(string targetPath);
    string GetUserFolder();
}

Enumerations

public enum LaunchType
{
    Launch,         // Adapter should launch and debug a new process
    Attach,         // Adapter should attach to an existing process
    NonDebugLaunch  // Adapter should launch without debugging (e.g. Ctrl+F5)
}

public enum LaunchLocation
{
    Local,  // Adapter runs on the local machine
    Remote  // Adapter runs on a remote UNIX host (SSH, Docker Linux, or WSL)
}

public enum TargetPlatform
{
    Unknown,
    Windows,
    Linux,
    OSX
}

Implementation Steps

1. Create a class that implements IAdapterLauncher

using System;
using System.Diagnostics;
using System.IO;
using Microsoft.VisualStudio.Debugger.DebugAdapterHost.Interfaces;

public class MyCustomLauncher : IAdapterLauncher
{
    private IDebugAdapterHostContext context;

    public void Initialize(IDebugAdapterHostContext context)
    {
        this.context = context;
    }

    public void UpdateLaunchOptions(IAdapterLaunchInfo launchInfo)
    {
        // Optional: inspect or modify launchInfo.LaunchJson before the adapter
        // is launched. For example, you could inject additional configuration
        // fields or generate the entire launch configuration for an attach
        // scenario based on launchInfo.AttachProcessId.
    }

    public ITargetHostProcess LaunchAdapter(
        IAdapterLaunchInfo launchInfo,
        ITargetHostInterop targetInterop)
    {
        // Option A: Use targetInterop to launch a process and return it directly.
        //   The returned ITargetHostProcess's StandardInput/StandardOutput will be
        //   used for DAP communication.
        // return targetInterop.ExecuteCommandAsync("/path/to/adapter", "--args");

        // Option B: Create a fully custom transport by returning your own
        //   ITargetHostProcess implementation that wraps arbitrary streams.
        return new MyCustomTransportProcess(/* ... */);

        // Option C: Return null to fall back to the default launch behavior
        //   (launching the adapter executable specified in the AD7Metrics registration).
    }
}

2. Implement ITargetHostProcess for a custom transport

If your scenario requires more than simply launching a process (for example, communicating over a network socket or named pipe), implement ITargetHostProcess to wrap your custom streams:

public class MyCustomTransportProcess : ITargetHostProcess
{
    private readonly Stream inputStream;
    private readonly Stream outputStream;
    private bool hasExited;

    public MyCustomTransportProcess(Stream inputStream, Stream outputStream)
    {
        // inputStream: the Debug Adapter Host will WRITE DAP messages here
        //              (this is "stdin" from the adapter's perspective)
        // outputStream: the Debug Adapter Host will READ DAP messages from here
        //               (this is "stdout" from the adapter's perspective)
        this.inputStream = inputStream;
        this.outputStream = outputStream;
    }

    public IntPtr Handle => IntPtr.Zero; // No local process handle

    public Stream StandardInput => this.inputStream;

    public Stream StandardOutput => this.outputStream;

    public bool HasExited => this.hasExited;

    public event EventHandler Exited;

    public event DataReceivedEventHandler ErrorDataReceived;

    public void Terminate()
    {
        this.hasExited = true;
        this.inputStream?.Close();
        this.outputStream?.Close();
        this.Exited?.Invoke(this, EventArgs.Empty);
    }
}

Important: The StandardInput and StandardOutput streams are used by the Debug Adapter Host for DAP protocol communication. StandardInput is the stream that the host writes to (sending messages to the adapter), and StandardOutput is the stream the host reads from (receiving messages from the adapter). These names reflect the perspective of the debug adapter process, not the host.

3. Register the launcher via AD7Metrics

Your IAdapterLauncher implementation must be registered in the Visual Studio registry so the Debug Adapter Host can discover and instantiate it. This is done in your extension's .pkgdef file.

First, register the CLSID of your implementation:

; Type registration for the custom launcher
[$RootKey$\CLSID\{YOUR-LAUNCHER-GUID}]
"Assembly"="MyExtension"
"Class"="MyExtension.MyCustomLauncher"
"CodeBase"="$PackageFolder$\MyExtension.dll"

Then, reference it from your debug engine's AD7Metrics registration. You can use either the AdapterLauncher property or the newer ExtensibilityObjects section:

Option A: Using ExtensibilityObjects section (preferred)

The ExtensibilityObjects section allows you to register multiple extensibility components (e.g., both an IAdapterLauncher and an ICustomProtocolExtension) in a single place:

[$RootKey$\AD7Metrics\Engine\{YOUR-ENGINE-GUID}\ExtensibilityObjects]
"MyLauncher"="{YOUR-LAUNCHER-GUID}"

Option B: Using AdapterLauncher property (simple)

[$RootKey$\AD7Metrics\Engine\{YOUR-ENGINE-GUID}]
; ... other engine metrics ...
"AdapterLauncher"="{YOUR-LAUNCHER-GUID}"

4. Full pkgdef example

Below is a minimal .pkgdef that registers a debug engine with a custom adapter launcher:

; Debug engine registration
[$RootKey$\AD7Metrics\Engine\{YOUR-ENGINE-GUID}]

; Required: Use the Debug Adapter Host engine CLSID
"CLSID"="{DAB324E9-7B35-454C-ACA8-F6BB0D5C8673}"
"AlwaysLoadLocal"=dword:00000001
"AddressBP"=dword:00000000
"CallStackBP"=dword:00000000

; Engine display name
"Name"="My Custom Debug Engine"

; Path to the debug adapter (used as fallback if LaunchAdapter returns null)
"Adapter"="$PackageFolder$\MyAdapter.exe"

; Language name shown in tool windows
"Language"="MyLanguage"

; Register the custom adapter launcher
[$RootKey$\AD7Metrics\Engine\{YOUR-ENGINE-GUID}\ExtensibilityObjects]
"MyLauncher"="{YOUR-LAUNCHER-GUID}"

; Support attaching to processes (optional)
; "Attach"=dword:00000001
; "PortSupplier"="{YOUR-PORT-SUPPLIER-GUID}"

; CLSID registration for the custom launcher
[$RootKey$\CLSID\{YOUR-LAUNCHER-GUID}]
"Assembly"="MyExtension"
"Class"="MyExtension.MyCustomLauncher"
"CodeBase"="$PackageFolder$\MyExtension.dll"

Common Scenarios

Launching an adapter on a remote UNIX host

When the Debug Adapter Host is launched through one of the built-in UNIX transports (SSH, Docker Linux, or WSL), launchInfo.LaunchLocation will be LaunchLocation.Remote and the targetInterop parameter provides methods to interact with that remote UNIX host. You can use targetInterop.ExecuteCommandAsync() to start the adapter on the remote system and get back an ITargetHostProcess whose streams are tunneled through the connection:

public ITargetHostProcess LaunchAdapter(
    IAdapterLaunchInfo launchInfo,
    ITargetHostInterop targetInterop)
{
    if (launchInfo.LaunchLocation == LaunchLocation.Remote)
    {
        // Deploy the adapter to the remote UNIX host if needed
        targetInterop.CopyFile(localAdapterPath, remoteAdapterPath);

        // Launch on the remote host — streams are automatically tunneled
        return targetInterop.ExecuteCommandAsync(remoteAdapterPath, "--interpreter=vscode");
    }

    return null; // Fall back to default for local launches
}

Wrapping a network socket

If your debug adapter communicates over TCP, you can connect a socket and wrap its NetworkStream in an ITargetHostProcess:

public ITargetHostProcess LaunchAdapter(
    IAdapterLaunchInfo launchInfo,
    ITargetHostInterop targetInterop)
{
    var client = new TcpClient("remote-host", 4711);
    var stream = client.GetStream();

    // NetworkStream is bidirectional, so use it for both input and output
    return new MyCustomTransportProcess(
        inputStream: stream,
        outputStream: stream);
}

Generating attach configuration

When supporting "Attach to Process", use UpdateLaunchOptions to generate the LaunchJson that the adapter expects:

public void UpdateLaunchOptions(IAdapterLaunchInfo launchInfo)
{
    if (launchInfo.LaunchType == LaunchType.Attach)
    {
        launchInfo.LaunchJson = $@"{{
            ""name"": "".NET Core Attach"",
            ""type"": ""coreclr"",
            ""request"": ""attach"",
            ""processId"": ""{launchInfo.AttachProcessId}""
        }}";
    }
}

Using the Logger

Your launcher can write diagnostic messages through the IDebugAdapterHostLogger available from the context:

public void Initialize(IDebugAdapterHostContext context)
{
    this.context = context;
}

public ITargetHostProcess LaunchAdapter(
    IAdapterLaunchInfo launchInfo,
    ITargetHostInterop targetInterop)
{
    // Messages logged with LogCategory.Launch are shown to the user if the
    // adapter fails to start, even when logging is not explicitly enabled.
    this.context.Logger.Log(
        "Connecting to remote adapter...",
        LogCategory.Launch);

    // ...
}

To enable the full Debug Adapter Host log for troubleshooting, run the following command in the Visual Studio Command Window (View > Other Windows > Command Window):

DebugAdapterHost.Logging /On /OutputWindow

Using Custom Engine Metrics

You can define custom metrics in your AD7Metrics registration and retrieve them at runtime via IAdapterLaunchInfo.GetMetricString(). This is useful for storing configuration values that your launcher needs:

; In your .pkgdef:
[$RootKey$\AD7Metrics\Engine\{YOUR-ENGINE-GUID}]
"MyCustomSetting"="some-value"
public ITargetHostProcess LaunchAdapter(
    IAdapterLaunchInfo launchInfo,
    ITargetHostInterop targetInterop)
{
    string mySetting = launchInfo.GetMetricString("MyCustomSetting");
    // Use the setting...
}

Notes

  • Only one IAdapterLauncher may be registered per debug engine. If more than one is found, the Debug Adapter Host will disable custom launch for that engine.
  • If LaunchAdapter returns null, the Debug Adapter Host falls back to its default behavior: launching the executable specified in the Adapter metric from the engine's AD7Metrics registration.
  • The Handle property on ITargetHostProcess is used to associate a local process with a job object so it can be cleaned up if Visual Studio exits unexpectedly. Set this to IntPtr.Zero if your transport does not correspond to a local process.
  • The Exited event on ITargetHostProcess should be raised when the transport connection is lost or the adapter process terminates. The Debug Adapter Host uses this to detect unexpected adapter exits.
  • ErrorDataReceived can be raised to report error messages from the adapter process. These are logged to the Debug Adapter Host log.