registry.ts•13.1 kB
import { execa } from 'execa';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { Logger } from '../server/logger';
import { ConfigManager } from '../server/config';
import { MetricsCollector } from '../server/metrics';
import { AdapterConfig, AdapterDiscovery, AdapterHealthCheck } from '../schemas/adapter.schemas';
import { createError } from '../server/error-taxonomy';
/**
* Adapter Registry and Locator for Windows
*
* Discovers, validates, and manages Windows-specific debug adapters
* with detailed diagnostics and user-friendly error messages.
*/
export class AdapterRegistry {
private logger: Logger;
private config: ConfigManager;
private metrics: MetricsCollector;
private cachedAdapters: Map<string, AdapterConfig> = new Map();
private healthChecks: Map<string, AdapterHealthCheck> = new Map();
private lastCacheUpdate: number = 0;
constructor() {
this.logger = new Logger('AdapterRegistry');
this.config = ConfigManager.getInstance();
this.metrics = MetricsCollector.getInstance();
}
/**
* Discover all available Windows debug adapters
*/
async discoverAdapters(): Promise<AdapterDiscovery> {
this.logger.info('Discovering Windows debug adapters...');
this.metrics.startTimer('adapters.discover');
const discovery: AdapterDiscovery = {
adapters: [],
errors: [],
missingAdapters: [],
foundAdapters: [],
};
try {
const windowsAdapters = this.config.get('adapters.windows', {});
for (const [adapterType, config] of Object.entries(windowsAdapters)) {
try {
const adapter = await this.validateAdapter(adapterType, config as AdapterConfig);
if (adapter) {
discovery.adapters.push(adapter);
discovery.foundAdapters.push(adapterType);
this.cachedAdapters.set(adapterType, adapter);
} else {
discovery.missingAdapters.push(adapterType);
}
} catch (error) {
this.logger.error(`Error validating ${adapterType} adapter:`, error);
discovery.errors.push({
adapter: adapterType,
error: (error as Error).message,
suggestion: this.getAdapterSuggestion(adapterType, error as Error),
});
}
}
// Update health checks
await this.updateHealthChecks(discovery.adapters);
this.metrics.increment('adapters.discover.count');
this.metrics.stopTimer('adapters.discover');
this.logger.info(
`Discovery complete: ${discovery.foundAdapters.length}/${Object.keys(windowsAdapters).length} adapters found`
);
} catch (error) {
this.logger.error('Adapter discovery failed:', error);
throw createError('ADAPTER_001');
}
return discovery;
}
/**
* Validate and get adapter configuration
*/
async getAdapter(adapterType: string): Promise<AdapterConfig | null> {
// Check cache first
const cached = this.cachedAdapters.get(adapterType);
if (cached && Date.now() - this.lastCacheUpdate < 300000) {
// 5 minute cache
return cached;
}
// Re-discover if needed
const discovery = await this.discoverAdapters();
return discovery.adapters.find(adapter => adapter.type === adapterType) || null;
}
/**
* Validate a specific adapter configuration
*/
private async validateAdapter(
type: string,
config: AdapterConfig
): Promise<AdapterConfig | null> {
this.logger.debug(`Validating ${type} adapter...`);
// Check if adapter path exists and is executable
let adapterPath = this.resolveAdapterPath(config.path) || '';
// Special handling for VS Code extensions
if (type === 'vscode-js-debug') {
adapterPath = (await this.resolveVSCodeJSDebug()) || '';
if (!adapterPath) {
throw createError('ADAPTER_001', { adapterType: type });
}
}
// Verify the path exists
if (!this.isPathValid(adapterPath)) {
throw createError('ADAPTER_001', { adapterType: type, path: adapterPath });
}
// Check version if available
if (config.windows.versionRequirements) {
await this.checkAdapterVersion(
type,
adapterPath,
config.windows.versionRequirements as { min: string; recommended: string }
);
}
// Test basic launchability
await this.testAdapterLaunch(type, adapterPath);
// Return updated config with resolved path
const validatedConfig: AdapterConfig = {
...config,
path: adapterPath,
name: config.name || type,
};
this.logger.info(`✓ ${type} adapter validated successfully`);
return validatedConfig;
}
/**
* Resolve adapter path with environment variable expansion
*/
private resolveAdapterPath(path: string): string {
if (!path) return '';
// Expand Windows environment variables
let resolvedPath = path.replace(/%([^%]+)%/g, (match, varName) => {
return process.env[varName] || match;
});
// Expand user home directory
resolvedPath = resolvedPath.replace(/%USERPROFILE%/gi, homedir());
resolvedPath = resolvedPath.replace(/%HOME%/gi, homedir());
return resolvedPath;
}
/**
* Resolve VS Code JavaScript Debugger path
*/
private async resolveVSCodeJSDebug(): Promise<string | null> {
const userExtPath = `${homedir()}\\.vscode\\extensions`;
const path = this.resolveAdapterPath(userExtPath);
if (!existsSync(path)) {
this.logger.warn(`VS Code extensions directory not found: ${path}`);
return null;
}
try {
const fs = await import('fs');
const extensions = fs.readdirSync(path);
// Look for js-debug extension
const jsDebugExtensions = extensions.filter(ext => ext.startsWith('ms-vscode.js-debug'));
if (jsDebugExtensions.length === 0) {
this.logger.warn('JavaScript Debugger extension not found in VS Code extensions');
return null;
}
// Use the latest version
const latestExtension = jsDebugExtensions.sort().pop()!;
const extensionPath = `${path}\\${latestExtension}\\out\\node\\debugAdapter.js`;
if (existsSync(extensionPath)) {
this.logger.info(`Found VS Code JS Debug at: ${extensionPath}`);
return extensionPath;
}
} catch (error) {
this.logger.error('Error resolving VS Code JS Debug:', error);
}
return null;
}
/**
* Check if path is valid and accessible
*/
private isPathValid(path: string): boolean {
if (!path || path.trim() === '') {
return false;
}
try {
return existsSync(path);
} catch (error) {
return false;
}
}
/**
* Check adapter version requirements
*/
private async checkAdapterVersion(
type: string,
_path: string,
requirements: { min: string; recommended: string }
): Promise<void> {
// For most adapters, we'll just check existence
// Version checking would be adapter-specific
this.logger.debug(`Version checking for ${type} (min: ${requirements.min})`);
// Placeholder for actual version checking logic
// This would need to be implemented per adapter type
}
/**
* Test adapter launch capability
*/
private async testAdapterLaunch(type: string, path: string): Promise<void> {
this.logger.debug(`Testing launch for ${type}...`);
// For VS Code JS Debug, test with Node.js
if (type === 'vscode-js-debug') {
try {
await execa('node', [path, '--version']);
} catch (error) {
throw new Error(`Failed to launch ${type}: ${(error as Error).message}`);
}
return;
}
// For other adapters, just test basic file accessibility
if (!existsSync(path)) {
throw new Error(`Adapter executable not found: ${path}`);
}
// Test file readability
try {
await import('fs').then(fs => fs.promises.access(path, fs.constants.R_OK));
} catch (error) {
throw new Error(`Cannot read adapter executable: ${path}`);
}
}
/**
* Get user-friendly suggestion for adapter errors
*/
private getAdapterSuggestion(type: string, error: Error): string {
const suggestions: Record<string, string> = {
vsdbg: 'Install Visual Studio 2022 with "Desktop development with C#" workload',
netcoredbg: 'Install .NET SDK from https://dotnet.microsoft.com/download',
'vscode-js-debug': 'Install VS Code and JavaScript Debugger extension',
debugpy: 'Install Python and run: pip install debugpy',
dap: 'Install compatible DAP adapter and place in PATH',
};
const baseSuggestion = suggestions[type] || `Install ${type} debug adapter`;
return `${baseSuggestion}. Error details: ${error.message}`;
}
/**
* Update health checks for all adapters
*/
private async updateHealthChecks(adapters: AdapterConfig[]): Promise<void> {
for (const adapter of adapters) {
const healthCheck = await this.performHealthCheck(adapter);
this.healthChecks.set(adapter.type, healthCheck);
}
}
/**
* Perform health check on adapter
*/
async performHealthCheck(adapter: AdapterConfig): Promise<AdapterHealthCheck> {
// const startTime = Date.now(); // Unused
let status: 'healthy' | 'unhealthy' | 'timeout' | 'error' = 'healthy';
let message = 'Adapter is healthy';
const details: any = {};
try {
// Check if path still exists
if (!existsSync(adapter.path)) {
status = 'unhealthy';
message = 'Adapter executable not found';
details.path = adapter.path;
} else {
// Test adapter launch with timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Health check timeout')), 5000);
});
const testPromise = this.testAdapterLaunch(adapter.type, adapter.path);
await Promise.race([testPromise, timeoutPromise]);
}
} catch (error) {
const errorMessage = (error as Error).message;
status = errorMessage === 'Health check timeout' ? 'timeout' : 'error';
message = errorMessage;
details.error = errorMessage;
}
return {
adapter: adapter.type,
status,
message,
details,
lastCheck: Date.now(),
};
}
/**
* Get health check status for adapter
*/
getHealthCheck(adapterType: string): AdapterHealthCheck | null {
return this.healthChecks.get(adapterType) || null;
}
/**
* Get all health checks
*/
getAllHealthChecks(): Map<string, AdapterHealthCheck> {
return new Map(this.healthChecks);
}
/**
* Clear adapter cache
*/
clearCache(): void {
this.cachedAdapters.clear();
this.healthChecks.clear();
this.lastCacheUpdate = 0;
this.logger.info('Adapter cache cleared');
}
/**
* Get Windows-specific adapter diagnostics
*/
getWindowsDiagnostics(): {
adapters: { [type: string]: { found: boolean; path: string; health?: AdapterHealthCheck } };
systemInfo: { [key: string]: string };
recommendations: string[];
} {
const diagnostics = {
adapters: {} as {
[type: string]: { found: boolean; path: string; health?: AdapterHealthCheck };
},
systemInfo: {
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
envPath: process.env['PATH']?.substring(0, 100) + '...',
},
recommendations: [] as string[],
};
// Get adapter information
const windowsAdapters = this.config.get('adapters.windows', {});
for (const [type, config] of Object.entries(windowsAdapters)) {
const healthCheck = this.getHealthCheck(type);
diagnostics.adapters[type] = {
found: healthCheck?.status === 'healthy',
path: this.resolveAdapterPath((config as any).path || ''),
health: healthCheck || {
status: 'unhealthy' as const,
message: 'Health check unavailable',
adapter: type,
lastCheck: Date.now(),
},
};
}
// Generate recommendations
for (const [type, info] of Object.entries(diagnostics.adapters)) {
if (!info.found) {
switch (type) {
case 'vsdbg':
diagnostics.recommendations.push(
'Install Visual Studio with "Desktop development with C#" workload'
);
break;
case 'netcoredbg':
diagnostics.recommendations.push(
'Install .NET SDK: https://dotnet.microsoft.com/download'
);
break;
case 'debugpy':
diagnostics.recommendations.push('Install Python and run: pip install debugpy');
break;
case 'vscode-js-debug':
diagnostics.recommendations.push('Install VS Code with JavaScript Debugger extension');
break;
}
}
}
return diagnostics;
}
}
/**
* Create adapter registry instance
*/
export function createAdapterRegistry(): AdapterRegistry {
return new AdapterRegistry();
}