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:
@@ -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);
|
||||||
|
|||||||
138
RoslynBridge/Services/RegistrationService.cs
Normal file
138
RoslynBridge/Services/RegistrationService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user