UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

601 lines (600 loc) 24.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PlatformWatcher = void 0; exports.createPlatformWatcher = createPlatformWatcher; exports.testPlatformWatching = testPlatformWatching; exports.getPlatformCapabilities = getPlatformCapabilities; const os = __importStar(require("os")); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const chokidar = __importStar(require("chokidar")); const events_1 = require("events"); const error_handler_1 = require("./error-handler"); // Cross-platform file watcher with intelligent fallbacks class PlatformWatcher extends events_1.EventEmitter { constructor(fallbackOptions = {}) { super(); this.activeWatchers = new Map(); this.fallbackWatchers = new Map(); this.watcherHealth = new Map(); this.healthCheckTimer = null; this.isActive = false; this.capabilities = this.detectPlatformCapabilities(); this.fallbackOptions = { primaryMethod: this.capabilities.recommendedWatchMethod, fallbackMethods: this.capabilities.fallbackMethods, fallbackDelay: 5000, maxRetries: 3, healthCheckInterval: 30000, enableFallbackLogging: true, platformOptimizations: true, adaptivePolling: true, ...fallbackOptions }; this.setupHealthChecking(); } // Detect platform capabilities and limitations detectPlatformCapabilities() { const platform = os.platform(); const arch = os.arch(); const baseCapabilities = { platform, architecture: arch, supportsNativeWatching: true, supportsPolling: true, supportsFSEvents: false, supportsInotify: false, maxWatchedFiles: 8192, recommendedWatchMethod: 'native', fallbackMethods: ['polling'], limitations: [] }; switch (platform) { case 'darwin': // macOS return { ...baseCapabilities, supportsFSEvents: true, maxWatchedFiles: 524288, // Higher limit on macOS recommendedWatchMethod: 'fsevents', fallbackMethods: ['native', 'polling'], limitations: [ 'FSEvents may have latency with network drives', 'Case sensitivity issues on case-insensitive filesystems' ] }; case 'linux': return { ...baseCapabilities, supportsInotify: true, maxWatchedFiles: this.getLinuxMaxWatchedFiles(), recommendedWatchMethod: 'inotify', fallbackMethods: ['native', 'polling'], limitations: [ 'inotify watch limit may be exceeded with large projects', 'NFS and some network filesystems may not work reliably' ] }; case 'win32': // Windows return { ...baseCapabilities, maxWatchedFiles: 65536, recommendedWatchMethod: 'native', fallbackMethods: ['polling'], limitations: [ 'Path length limitations (260 characters)', 'Case insensitive filesystem', 'Some antivirus software may interfere' ] }; case 'freebsd': case 'openbsd': case 'netbsd': return { ...baseCapabilities, maxWatchedFiles: 4096, recommendedWatchMethod: 'polling', fallbackMethods: ['native'], limitations: [ 'Limited native watching support', 'Polling recommended for reliability' ] }; default: return { ...baseCapabilities, maxWatchedFiles: 1024, recommendedWatchMethod: 'polling', fallbackMethods: ['native'], limitations: [ 'Unknown platform - using conservative defaults', 'Native watching may not be reliable' ] }; } } // Get Linux inotify limits getLinuxMaxWatchedFiles() { try { const maxUserWatches = fs.readFileSync('/proc/sys/fs/inotify/max_user_watches', 'utf8'); return parseInt(maxUserWatches.trim(), 10) || 8192; } catch { return 8192; // Default fallback } } // Create platform-optimized watcher async createWatcher(watchPath, options = {}) { const watcherId = this.generateWatcherId(watchPath); try { // Apply platform optimizations const optimizedOptions = this.applyPlatformOptimizations(options); // Create primary watcher const watcher = await this.createPrimaryWatcher(watchPath, optimizedOptions); // Set up health monitoring this.setupWatcherHealthMonitoring(watcherId, watcher, watchPath); // Store watcher this.activeWatchers.set(watcherId, watcher); // Set up fallback if enabled if (options.enableFallbacks !== false) { await this.setupFallbackWatcher(watcherId, watchPath, optimizedOptions); } this.emit('watcher-created', { watcherId, watchPath, method: this.fallbackOptions.primaryMethod }); return watcher; } catch (error) { this.emit('watcher-error', { watcherId, watchPath, error }); // Try fallback methods if (options.enableFallbacks !== false) { return this.createFallbackWatcher(watcherId, watchPath, options); } throw new error_handler_1.ValidationError(`Failed to create watcher for ${watchPath}: ${error instanceof Error ? error.message : String(error)}`); } } // Apply platform-specific optimizations applyPlatformOptimizations(options) { const baseOptions = { persistent: true, ignoreInitial: true, followSymlinks: false, ignorePermissionErrors: true, atomic: true }; // Platform-specific optimizations const platformOptions = options.platformSpecific?.[this.capabilities.platform] || {}; switch (this.capabilities.platform) { case 'darwin': return { ...baseOptions, usePolling: options.usePolling || false, interval: options.interval || 1000, binaryInterval: options.binaryInterval || 3000, alwaysStat: false, // FSEvents provides stat info awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100 }, ...platformOptions, ...options }; case 'linux': return { ...baseOptions, usePolling: options.usePolling || false, interval: options.interval || 1000, binaryInterval: options.binaryInterval || 3000, alwaysStat: true, // inotify needs stat calls awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 100 }, ...platformOptions, ...options }; case 'win32': return { ...baseOptions, usePolling: options.usePolling || false, interval: options.interval || 1000, binaryInterval: options.binaryInterval || 3000, alwaysStat: true, awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100 }, ...platformOptions, ...options }; default: return { ...baseOptions, usePolling: options.usePolling || true, // Default to polling for unknown platforms interval: options.interval || 2000, binaryInterval: options.binaryInterval || 5000, alwaysStat: true, ...platformOptions, ...options }; } } // Create primary watcher with method detection async createPrimaryWatcher(watchPath, options) { const method = this.fallbackOptions.primaryMethod; switch (method) { case 'fsevents': if (!this.capabilities.supportsFSEvents) { throw new Error('FSEvents not supported on this platform'); } return chokidar.watch(watchPath, { ...options, usePolling: false }); case 'inotify': if (!this.capabilities.supportsInotify) { throw new Error('inotify not supported on this platform'); } return chokidar.watch(watchPath, { ...options, usePolling: false }); case 'polling': return chokidar.watch(watchPath, { ...options, usePolling: true, interval: this.getAdaptivePollingInterval(watchPath) }); case 'hybrid': return this.createHybridWatcher(watchPath, options); case 'native': default: return chokidar.watch(watchPath, { ...options, usePolling: false }); } } // Create hybrid watcher (combines multiple methods) async createHybridWatcher(watchPath, options) { // For now, hybrid mode uses native with polling fallback // This could be enhanced to run multiple watchers simultaneously try { return chokidar.watch(watchPath, { ...options, usePolling: false }); } catch (error) { if (this.fallbackOptions.enableFallbackLogging) { console.warn(`Hybrid watcher falling back to polling for ${watchPath}: ${error}`); } return chokidar.watch(watchPath, { ...options, usePolling: true }); } } // Setup fallback watcher async setupFallbackWatcher(watcherId, watchPath, options) { // Prepare fallback but don't activate unless primary fails const fallbackMethod = this.fallbackOptions.fallbackMethods[0]; if (!fallbackMethod) return; try { // Create fallback options const fallbackOptions = { ...options, usePolling: fallbackMethod === 'polling' }; // Store fallback configuration for later activation this.watcherHealth.set(watcherId, { isHealthy: true, lastCheck: Date.now(), failureCount: 0, fallbackReady: true, fallbackOptions: { watchPath, options: fallbackOptions } }); } catch (error) { if (this.fallbackOptions.enableFallbackLogging) { console.warn(`Failed to prepare fallback watcher for ${watchPath}: ${error}`); } } } // Create fallback watcher when primary fails async createFallbackWatcher(watcherId, watchPath, options) { const fallbackMethods = this.fallbackOptions.fallbackMethods; for (const method of fallbackMethods) { try { if (this.fallbackOptions.enableFallbackLogging) { console.log(`Attempting fallback watcher method: ${method} for ${watchPath}`); } let fallbackOptions; switch (method) { case 'polling': fallbackOptions = { ...this.applyPlatformOptimizations(options), usePolling: true, interval: this.getAdaptivePollingInterval(watchPath) }; break; case 'native': fallbackOptions = { ...this.applyPlatformOptimizations(options), usePolling: false }; break; default: fallbackOptions = this.applyPlatformOptimizations(options); } const watcher = chokidar.watch(watchPath, fallbackOptions); // Store as fallback watcher this.fallbackWatchers.set(watcherId, watcher); this.activeWatchers.set(watcherId, watcher); this.emit('fallback-activated', { watcherId, watchPath, method }); return watcher; } catch (error) { if (this.fallbackOptions.enableFallbackLogging) { console.warn(`Fallback method ${method} failed for ${watchPath}: ${error}`); } continue; } } throw new error_handler_1.ValidationError(`All fallback methods failed for ${watchPath}`); } // Get adaptive polling interval based on directory size getAdaptivePollingInterval(watchPath) { if (!this.fallbackOptions.adaptivePolling) { return 1000; // Default 1 second } try { // Estimate directory complexity const stats = fs.statSync(watchPath); if (stats.isFile()) { return 500; // Fast polling for single files } // For directories, estimate based on size heuristics // This is a simple implementation - could be more sophisticated const entries = fs.readdirSync(watchPath); const fileCount = entries.length; if (fileCount < 50) { return 500; // Small directory - fast polling } else if (fileCount < 200) { return 1000; // Medium directory - normal polling } else { return 2000; // Large directory - slower polling } } catch (error) { return 1000; // Default on error } } // Setup watcher health monitoring setupWatcherHealthMonitoring(watcherId, watcher, watchPath) { const health = { isHealthy: true, lastCheck: Date.now(), failureCount: 0, fallbackReady: false }; this.watcherHealth.set(watcherId, health); // Monitor watcher events for health watcher.on('error', (error) => { health.isHealthy = false; health.failureCount++; health.lastError = error; this.emit('watcher-unhealthy', { watcherId, watchPath, error, failureCount: health.failureCount }); // Trigger fallback if failure count exceeds threshold if (health.failureCount >= this.fallbackOptions.maxRetries) { this.activateFallback(watcherId, watchPath).catch(err => { this.emit('fallback-failed', { watcherId, watchPath, error: err }); }); } }); watcher.on('ready', () => { health.isHealthy = true; health.lastCheck = Date.now(); health.failureCount = 0; delete health.lastError; }); } // Activate fallback watcher async activateFallback(watcherId, watchPath) { const health = this.watcherHealth.get(watcherId); if (!health?.fallbackReady || !health.fallbackOptions) { throw new Error('No fallback available'); } try { // Close primary watcher const primaryWatcher = this.activeWatchers.get(watcherId); if (primaryWatcher) { await primaryWatcher.close(); } // Create and activate fallback const fallbackWatcher = await this.createFallbackWatcher(watcherId, health.fallbackOptions.watchPath, health.fallbackOptions.options); this.activeWatchers.set(watcherId, fallbackWatcher); if (this.fallbackOptions.enableFallbackLogging) { console.log(`Activated fallback watcher for ${watchPath}`); } } catch (error) { throw new error_handler_1.ValidationError(`Failed to activate fallback for ${watchPath}: ${error}`); } } // Setup periodic health checking setupHealthChecking() { if (this.fallbackOptions.healthCheckInterval <= 0) return; this.healthCheckTimer = setInterval(() => { this.performHealthCheck(); }, this.fallbackOptions.healthCheckInterval); } // Perform health check on all watchers performHealthCheck() { const now = Date.now(); for (const [watcherId, health] of this.watcherHealth.entries()) { health.lastCheck = now; // Check if watcher is responsive const watcher = this.activeWatchers.get(watcherId); if (!watcher) { health.isHealthy = false; continue; } // Simple health check - could be enhanced with actual file system operations try { // If watcher has listeners and hasn't errored recently, consider it healthy const hasListeners = watcher.listenerCount('change') > 0 || watcher.listenerCount('add') > 0 || watcher.listenerCount('unlink') > 0; if (hasListeners && !health.lastError) { health.isHealthy = true; health.failureCount = Math.max(0, health.failureCount - 1); // Slowly recover } } catch (error) { health.isHealthy = false; health.failureCount++; health.lastError = error; } } this.emit('health-check-completed', { totalWatchers: this.watcherHealth.size, healthyWatchers: Array.from(this.watcherHealth.values()).filter(h => h.isHealthy).length }); } // Get platform capabilities getPlatformCapabilities() { return { ...this.capabilities }; } // Get watcher health status getWatcherHealth(watcherId) { if (watcherId) { return this.watcherHealth.get(watcherId) || null; } return new Map(this.watcherHealth); } // Get active watchers count getActiveWatchersCount() { return this.activeWatchers.size; } // Close all watchers async closeAll() { this.isActive = false; // Clear health check timer if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); this.healthCheckTimer = null; } // Close all active watchers const closePromises = []; for (const [watcherId, watcher] of this.activeWatchers.entries()) { closePromises.push(watcher.close().catch(error => { console.warn(`Error closing watcher ${watcherId}: ${error}`); })); } // Close fallback watchers for (const [watcherId, watcher] of this.fallbackWatchers.entries()) { closePromises.push(watcher.close().catch(error => { console.warn(`Error closing fallback watcher ${watcherId}: ${error}`); })); } await Promise.all(closePromises); // Clear all maps this.activeWatchers.clear(); this.fallbackWatchers.clear(); this.watcherHealth.clear(); this.emit('all-watchers-closed'); } // Generate unique watcher ID generateWatcherId(watchPath) { const normalized = path.normalize(watchPath); const hash = require('crypto').createHash('md5').update(normalized).digest('hex').substr(0, 8); return `watcher_${hash}_${Date.now()}`; } // Test platform capabilities async testPlatformCapabilities() { const testDir = path.join(os.tmpdir(), `re-shell-watcher-test-${Date.now()}`); try { await fs.ensureDir(testDir); const results = { platform: this.capabilities.platform, nativeWatching: false, polling: false, fsevents: false, inotify: false, maxWatchedFiles: this.capabilities.maxWatchedFiles, recommendations: [] }; // Test native watching try { const nativeWatcher = chokidar.watch(testDir, { usePolling: false }); await new Promise(resolve => setTimeout(resolve, 100)); await nativeWatcher.close(); results.nativeWatching = true; } catch (error) { results.recommendations.push('Native file watching is not available - use polling'); } // Test polling try { const pollingWatcher = chokidar.watch(testDir, { usePolling: true, interval: 100 }); await new Promise(resolve => setTimeout(resolve, 100)); await pollingWatcher.close(); results.polling = true; } catch (error) { results.recommendations.push('Polling is not available - this is unusual'); } // Platform-specific tests if (this.capabilities.platform === 'darwin') { results.fsevents = results.nativeWatching; // FSEvents is the native method on macOS } if (this.capabilities.platform === 'linux') { results.inotify = results.nativeWatching; // inotify is the native method on Linux } // Generate recommendations if (!results.nativeWatching && results.polling) { results.recommendations.push('Use polling-based file watching for reliability'); } if (this.capabilities.maxWatchedFiles < 8192) { results.recommendations.push('Consider increasing system file watch limits for large projects'); } return results; } finally { await fs.remove(testDir); } } } exports.PlatformWatcher = PlatformWatcher; // Utility functions function createPlatformWatcher(options) { return new PlatformWatcher(options); } async function testPlatformWatching() { const watcher = new PlatformWatcher(); return await watcher.testPlatformCapabilities(); } function getPlatformCapabilities() { const watcher = new PlatformWatcher(); return watcher.getPlatformCapabilities(); }