Add instance registration service for multi-client WebAPI integration

- Create RegistrationService to register VS instances with WebAPI
  - Registers on startup with: port, process ID, solution info
  - Reads WebApiUrl from ConfigurationService
  - Sends heartbeat every N seconds (configurable)
  - Unregisters on shutdown
  - Gracefully handles registration failures (VS extension works standalone)

- Update RoslynBridgePackage to use RegistrationService
  - Creates BridgeServer and gets actual port used
  - Registers with WebAPI using discovered port
  - Cleans up registration on disposal

This enables the WebAPI middleware to discover and route requests to
the correct Visual Studio instance based on solution path, port, or PID.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-26 23:51:20 -04:00
parent 1ad0a98d4d
commit 716827a665
2 changed files with 145 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ namespace RoslynBridge
{ {
public const string PackageGuidString = "b2c3d4e5-f6a7-4b5c-9d8e-0f1a2b3c4d5e"; public const string PackageGuidString = "b2c3d4e5-f6a7-4b5c-9d8e-0f1a2b3c4d5e";
private BridgeServer? _bridgeServer; private BridgeServer? _bridgeServer;
private Services.RegistrationService? _registrationService;
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress) protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{ {
@@ -29,6 +30,10 @@ namespace RoslynBridge
_bridgeServer = new BridgeServer(this); _bridgeServer = new BridgeServer(this);
await _bridgeServer.StartAsync(); await _bridgeServer.StartAsync();
// Register with WebAPI
_registrationService = new Services.RegistrationService(this, _bridgeServer.Port);
await _registrationService.RegisterAsync();
await base.InitializeAsync(cancellationToken, progress); await base.InitializeAsync(cancellationToken, progress);
} }
catch (Exception ex) catch (Exception ex)
@@ -43,6 +48,8 @@ namespace RoslynBridge
{ {
if (disposing) if (disposing)
{ {
_registrationService?.UnregisterAsync().Wait(TimeSpan.FromSeconds(5));
_registrationService?.Dispose();
_bridgeServer?.Dispose(); _bridgeServer?.Dispose();
} }
base.Dispose(disposing); base.Dispose(disposing);

View File

@@ -0,0 +1,138 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
namespace RoslynBridge.Services
{
/// <summary>
/// Service for registering this VS instance with the WebAPI
/// </summary>
public class RegistrationService : IDisposable
{
private readonly string _webApiUrl;
private readonly HttpClient _httpClient;
private readonly int _port;
private readonly AsyncPackage _package;
private System.Threading.Timer? _heartbeatTimer;
private bool _isRegistered;
public RegistrationService(AsyncPackage package, int port)
{
_package = package;
_port = port;
_webApiUrl = ConfigurationService.Instance.WebApiUrl;
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
}
public async Task RegisterAsync()
{
try
{
var dte = await GetDTEAsync();
var solutionPath = dte?.Solution?.FullName;
var solutionName = string.IsNullOrEmpty(solutionPath)
? null
: System.IO.Path.GetFileNameWithoutExtension(solutionPath);
var registrationData = new
{
port = _port,
processId = Process.GetCurrentProcess().Id,
solutionPath = string.IsNullOrEmpty(solutionPath) ? null : solutionPath,
solutionName = solutionName,
projects = new string[] { } // TODO: Get project names
};
var json = JsonSerializer.Serialize(registrationData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{_webApiUrl}/api/instances/register", content);
if (response.IsSuccessStatusCode)
{
_isRegistered = true;
Debug.WriteLine($"Successfully registered with WebAPI at {_webApiUrl}");
// Start heartbeat timer (configurable interval)
var heartbeatInterval = TimeSpan.FromSeconds(ConfigurationService.Instance.HeartbeatIntervalSeconds);
_heartbeatTimer = new System.Threading.Timer(
async _ => await SendHeartbeatAsync(),
null,
heartbeatInterval,
heartbeatInterval);
}
else
{
Debug.WriteLine($"Failed to register with WebAPI: {response.StatusCode}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error registering with WebAPI: {ex.Message}");
// Don't throw - registration is optional, VS extension should work standalone
}
}
private async Task SendHeartbeatAsync()
{
if (!_isRegistered)
return;
try
{
var processId = Process.GetCurrentProcess().Id;
var response = await _httpClient.PostAsync(
$"{_webApiUrl}/api/instances/heartbeat/{processId}",
null);
if (!response.IsSuccessStatusCode)
{
Debug.WriteLine($"Heartbeat failed: {response.StatusCode}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending heartbeat: {ex.Message}");
}
}
public async Task UnregisterAsync()
{
if (!_isRegistered)
return;
try
{
var processId = Process.GetCurrentProcess().Id;
var response = await _httpClient.PostAsync(
$"{_webApiUrl}/api/instances/unregister/{processId}",
null);
if (response.IsSuccessStatusCode)
{
Debug.WriteLine("Successfully unregistered from WebAPI");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error unregistering from WebAPI: {ex.Message}");
}
}
private async Task<EnvDTE.DTE?> GetDTEAsync()
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
return await _package.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
}
public void Dispose()
{
_heartbeatTimer?.Dispose();
_httpClient?.Dispose();
}
}
}