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,
ITargetHostInteropoperates 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 yourIAdapterLauncherimplementation and return anITargetHostProcessthat 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
IAdapterLaunchermay be registered per debug engine. If more than one is found, the Debug Adapter Host will disable custom launch for that engine. - If
LaunchAdapterreturnsnull, the Debug Adapter Host falls back to its default behavior: launching the executable specified in theAdaptermetric from the engine's AD7Metrics registration. - The
Handleproperty onITargetHostProcessis used to associate a local process with a job object so it can be cleaned up if Visual Studio exits unexpectedly. Set this toIntPtr.Zeroif your transport does not correspond to a local process. - The
Exitedevent onITargetHostProcessshould be raised when the transport connection is lost or the adapter process terminates. The Debug Adapter Host uses this to detect unexpected adapter exits. ErrorDataReceivedcan be raised to report error messages from the adapter process. These are logged to the Debug Adapter Host log.