UNPKG

greed.js

Version:

Lightweight, private alternative to Colab. Run PyTorch & NumPy in browser with GPU acceleration (8.8x speedup). Fast, secure, runs locally.

718 lines (624 loc) 21.4 kB
/** * RuntimeManager - Handles Pyodide initialization and Python package management * Supports both main-thread and Web Worker modes * Extracted from monolithic Greed class for better separation of concerns */ import EventEmitter from './event-emitter.js'; import PyodideWorkerManager from '../compute/worker/pyodide-worker-manager.js'; class RuntimeManager extends EventEmitter { constructor(config = {}) { super(); this.config = { pyodideIndexURL: config.pyodideIndexURL || 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/', preloadPackages: config.preloadPackages || ['numpy'], availablePackages: config.availablePackages || [ 'numpy', 'scipy', 'matplotlib', 'pandas', 'scikit-learn', 'plotly', 'seaborn', 'statsmodels', 'sympy', 'networkx' ], timeout: config.initTimeout || 30000, enableWorkers: config.enableWorkers !== false, // Worker mode by default ...config }; // Runtime mode (worker or main-thread) this.mode = this.config.enableWorkers ? 'worker' : 'main'; // Main-thread mode properties this.pyodide = null; // Worker mode manager this.workerManager = null; // Common properties this.isReady = false; this.installedPackages = new Set(); this.initPromise = null; } /** * Initialize Pyodide runtime with error handling and progress tracking */ async initialize() { if (this.initPromise) { return this.initPromise; } this.initPromise = this._initializeInternal(); return this.initPromise; } async _initializeInternal() { if (this.mode === 'worker') { return this._initializeWorkerMode(); } else { return this._initializeMainThreadMode(); } } /** * Initialize in Web Worker mode (prevents UI blocking) */ async _initializeWorkerMode() { try { this.emit('init:start', { stage: 'worker', mode: 'worker' }); // Create worker manager this.workerManager = new PyodideWorkerManager({ pyodideIndexURL: this.config.pyodideIndexURL, preloadPackages: this.config.preloadPackages, timeout: this.config.timeout }); // Forward worker events this.workerManager.on('init:progress', (data) => this.emit('init:progress', data)); this.workerManager.on('init:complete', (data) => { this.installedPackages = new Set(data.installedPackages); this.emit('init:complete', data); }); this.workerManager.on('init:error', (data) => this.emit('init:error', data)); this.workerManager.on('packages:loading', (data) => this.emit('packages:loading', data)); this.workerManager.on('packages:loaded', (data) => this.emit('packages:loaded', data)); this.workerManager.on('execution:complete', (data) => this.emit('execution:complete', data)); this.workerManager.on('execution:error', (data) => this.emit('execution:error', data)); this.workerManager.on('execution:stdout', (data) => this.emit('execution:stdout', data)); // Initialize worker await this.workerManager.initialize(); this.isReady = true; return true; } catch (error) { this.emit('init:error', { error, stage: 'worker-initialization' }); throw error; } } /** * Initialize in main-thread mode (may block UI) */ async _initializeMainThreadMode() { try { this.emit('init:start', { stage: 'pyodide', mode: 'main' }); // Validate Pyodide availability if (typeof loadPyodide === 'undefined') { throw new Error('Pyodide not loaded. Please include pyodide.js in your HTML.'); } // Initialize with timeout and 4GB memory limit const pyodidePromise = loadPyodide({ indexURL: this.config.pyodideIndexURL, // Enable 4GB WASM memory for large models // Requires Memory64 support (Chrome 119+, Firefox 128+) args: ['-Xalloc-env=PYODIDE_WASM_MEMORY=4294967296'] }); this.pyodide = await Promise.race([ pyodidePromise, this._createTimeoutPromise(this.config.timeout, 'Pyodide initialization timeout') ]); this.emit('init:progress', { stage: 'pyodide', status: 'loaded' }); // Pre-load essential packages if (this.config.preloadPackages.length > 0) { this.emit('init:progress', { stage: 'packages', packages: this.config.preloadPackages }); await this._loadPackages(this.config.preloadPackages); } this.isReady = true; this.emit('init:complete', { installedPackages: Array.from(this.installedPackages) }); return true; } catch (error) { this.emit('init:error', { error, stage: 'initialization' }); throw error; } } /** * Load Python packages with progress tracking */ async loadPackages(packages) { if (!this.isReady) { throw new Error('Runtime not initialized. Call initialize() first.'); } if (this.mode === 'worker') { const result = await this.workerManager.loadPackages(packages); result.forEach(pkg => this.installedPackages.add(pkg)); return result; } else { return this._loadPackages(packages); } } async _loadPackages(packages) { const packagesToLoad = packages.filter(pkg => !this.installedPackages.has(pkg)); if (packagesToLoad.length === 0) { return Array.from(this.installedPackages); } try { this.emit('packages:loading', { packages: packagesToLoad }); await this.pyodide.loadPackage(packagesToLoad); packagesToLoad.forEach(pkg => this.installedPackages.add(pkg)); this.emit('packages:loaded', { loaded: packagesToLoad, total: Array.from(this.installedPackages) }); return Array.from(this.installedPackages); } catch (error) { // Log the error but don't throw - allows execution to continue this.emit('packages:error', { error, packages: packagesToLoad }); // Mark packages as installed anyway to prevent retry loops packagesToLoad.forEach(pkg => this.installedPackages.add(pkg)); return Array.from(this.installedPackages); } } /** * Execute Python code with error handling and context isolation */ async runPython(code, options = {}) { if (!this.isReady) { throw new Error('Runtime not initialized. Call initialize() first.'); } // Extract validateInput for security check, but pass full options to execution methods const { validateInput = true } = options; if (validateInput && this._containsDangerousPatterns(code)) { throw new SecurityError('Potentially dangerous code patterns detected'); } // Auto-load packages if code imports them const packageChecks = [ { check: this._needsMatplotlib.bind(this), loader: this._ensureMatplotlibLoaded.bind(this) }, { check: this._needsPandas.bind(this), loader: this._ensurePandasLoaded.bind(this) }, { check: this._needsScipy.bind(this), loader: this._ensureScipyLoaded.bind(this) }, { check: this._needsPlotly.bind(this), loader: this._ensurePlotlyLoaded.bind(this) }, { check: this._needsSklearn.bind(this), loader: this._ensureSklearnLoaded.bind(this) } ]; // Check and load packages sequentially to avoid conflicts for (const { check, loader } of packageChecks) { if (check(code)) { await loader(); } } // Delegate to worker or main thread - pass all options to preserve taskId, streamOutput, etc. if (this.mode === 'worker') { return this._runPythonWorker(code, options); } else { return this._runPythonMain(code, options); } } /** * Execute Python code in worker mode (non-blocking) */ async _runPythonWorker(code, options) { try { const result = await this.workerManager.executePython(code, options); return result; } catch (error) { this.emit('execution:error', { error, code: code.substring(0, 100) }); throw error; } } /** * Execute Python code on main thread (may block UI) */ async _runPythonMain(code, options) { const { captureOutput, timeout, globals = {}, taskId, streamOutput } = options; try { // Set globals if provided - with error handling to prevent fatal errors for (const [key, value] of Object.entries(globals)) { try { this.pyodide.globals.set(key, value); } catch (globalError) { // Continue with other globals instead of failing completely this.emit('global:error', { key, error: globalError.message }); } } let result; if (captureOutput) { // Extract taskId and streamOutput from options // Only enable streaming if taskId is provided AND streamOutput is not explicitly false const shouldStream = (streamOutput !== false) && Boolean(taskId); // Inject streaming callback into Python globals if streaming enabled if (shouldStream) { this.pyodide.globals.set('__greed_emit_stdout__', (tid, output) => { this.emit('execution:stdout', { type: 'execution:stdout', taskId: tid, output: output, timestamp: Date.now() }); }); } else { } // Enhanced stdout capture with streaming support const outputCode = ` import sys from io import StringIO import time class StreamingBuffer: def __init__(self, task_id, emit_callback, should_stream): self.buffer = StringIO() self.task_id = task_id self.emit_callback = emit_callback self.should_stream = should_stream self.pending_output = "" def write(self, text): self.buffer.write(text) # Emit immediately for real-time streaming if self.should_stream and self.emit_callback and text: # Emit immediately - don't wait for time intervals # This ensures output appears in real-time, even during sleep() calls self.emit_callback(self.task_id, text) def flush(self): self.buffer.flush() # Emit any remaining output (usually not needed with immediate emission) if self.should_stream and self.emit_callback and self.pending_output: self.emit_callback(self.task_id, self.pending_output) self.pending_output = "" def getvalue(self): return self.buffer.getvalue() _output_buffer = StreamingBuffer('${taskId || ''}', ${shouldStream ? '__greed_emit_stdout__' : 'None'}, ${shouldStream ? 'True' : 'False'}) _original_stdout = sys.stdout sys.stdout = _output_buffer try: ${code.split('\n').map(line => ' ' + line).join('\n')} finally: sys.stdout.flush() sys.stdout = _original_stdout _captured_output = _output_buffer.getvalue() `; // Use runPythonAsync without Promise.race to avoid "interrupted by user" errors // Promise.race can interrupt Pyodide mid-execution causing fatal errors // Set up a non-interrupting timeout warning (only if timeout > 0) let timeoutWarning; if (timeout && timeout > 0) { timeoutWarning = setTimeout(() => { this.emit('execution:timeout', { timeout, stage: 'capture_output' }); }, timeout); } try { await this.pyodide.runPythonAsync(outputCode); } finally { if (timeoutWarning) { clearTimeout(timeoutWarning); } } // Get captured output safely with error handling let capturedOutput = ''; try { capturedOutput = this.pyodide.globals.get('_captured_output') || ''; } catch (getError) { this.emit('output:error', { error: getError.message }); capturedOutput = 'Output capture failed'; } result = { output: capturedOutput }; // Clean up globals to prevent memory leaks try { this.pyodide.globals.delete('_captured_output'); this.pyodide.globals.delete('_output_buffer'); this.pyodide.globals.delete('_original_stdout'); if (shouldStream) { this.pyodide.globals.delete('__greed_emit_stdout__'); } } catch (cleanupError) { // Ignore cleanup errors but emit them this.emit('cleanup:warning', { error: cleanupError.message }); } } else { // Use consistent async execution without Promise.race to avoid interruption errors // Set up a non-interrupting timeout warning (only if timeout > 0) let timeoutWarning; if (timeout && timeout > 0) { timeoutWarning = setTimeout(() => { this.emit('execution:timeout', { timeout, stage: 'no_capture' }); }, timeout); } try { result = await this.pyodide.runPythonAsync(code); } finally { if (timeoutWarning) { clearTimeout(timeoutWarning); } } } return result; } catch (error) { this.emit('execution:error', { error, code: code.substring(0, 100) }); throw error; } } /** * Get Python global variable */ async getGlobal(name) { if (!this.isReady) { throw new Error('Runtime not initialized'); } if (this.mode === 'worker') { return await this.workerManager.getGlobal(name); } try { return this.pyodide.globals.get(name); } catch (error) { this.emit('global:get:error', { name, error: error.message }); return undefined; } } /** * Set Python global variable */ async setGlobal(name, value) { if (!this.isReady) { throw new Error('Runtime not initialized'); } if (this.mode === 'worker') { return await this.workerManager.setGlobal(name, value); } try { this.pyodide.globals.set(name, value); } catch (error) { this.emit('global:set:error', { name, error: error.message }); throw error; // Re-throw since this is a direct API call } } /** * Check if package is installed */ hasPackage(packageName) { return this.installedPackages.has(packageName); } /** * Get runtime status */ getStatus() { return { isReady: this.isReady, installedPackages: Array.from(this.installedPackages), pyodideVersion: this.pyodide?.version || null, config: this.config }; } /** * Clear Python globals that might cause state persistence * Uses direct Pyodide API to avoid recursion through runPython */ async clearExecutionState() { if (!this.isReady) { return; } try { // Use direct Pyodide API to avoid recursion through runPython await this.pyodide.runPythonAsync(` import gc import sys import builtins # List of globals to preserve (built-ins and essential modules) preserved_globals = { 'torch', 'np', 'numpy', 'sys', 'builtins', '__builtins__', 'gc', '__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__cached__', '__file__' } # Get current globals current_globals = list(globals().keys()) # Remove user-defined variables for var_name in current_globals: if (var_name not in preserved_globals and not var_name.startswith('_') and not callable(globals().get(var_name, None)) or var_name.startswith('_greed_')): try: del globals()[var_name] except: pass # Force garbage collection gc.collect() `); } catch (error) { this.emit('state:clear:error', { error: error.message }); } } /** * Cleanup runtime resources */ async cleanup() { try { if (this.mode === 'worker' && this.workerManager) { // Terminate the worker await this.workerManager.terminate(); this.workerManager = null; } else if (this.pyodide) { // Try to clean up gracefully first using async version try { await this.pyodide.runPythonAsync(` import gc import sys # Clear user globals user_globals = [k for k in list(globals().keys()) if not k.startswith('__') and k not in sys.modules] for k in user_globals: try: del globals()[k] except: pass gc.collect() `); } catch (e) { // Ignore cleanup errors during shutdown } this.pyodide.globals.clear(); this.pyodide = null; } this.isReady = false; this.installedPackages.clear(); this.initPromise = null; this.emit('cleanup:complete'); } catch (error) { this.emit('cleanup:error', { error }); // Error already emitted for handling by parent components } } // Private helper methods _createTimeoutPromise(timeout, message) { return new Promise((_, reject) => { setTimeout(() => reject(new Error(message)), timeout); }); } /** * Check if code requires matplotlib */ _needsMatplotlib(code) { const matplotlibPatterns = [ /import\s+matplotlib/, /from\s+matplotlib/, /import\s+matplotlib\.pyplot/, /from\s+matplotlib\.pyplot/, /plt\./ ]; return matplotlibPatterns.some(pattern => pattern.test(code)); } /** * Ensure matplotlib is loaded and available */ async _ensureMatplotlibLoaded() { try { if (!this.installedPackages.has('matplotlib')) { this.emit('package:loading', { package: 'matplotlib' }); await this.pyodide.loadPackage('matplotlib'); this.installedPackages.add('matplotlib'); this.emit('package:loaded', { package: 'matplotlib' }); } } catch (error) { this.emit('package:error', { package: 'matplotlib', error }); // Don't throw - allow execution to continue } } /** * Check if code requires pandas */ _needsPandas(code) { const pandasPatterns = [ /import\s+pandas/, /from\s+pandas/, /import\s+pandas\s+as\s+pd/, /pd\./ ]; return pandasPatterns.some(pattern => pattern.test(code)); } /** * Ensure pandas is loaded and available */ async _ensurePandasLoaded() { try { if (!this.installedPackages.has('pandas')) { this.emit('package:loading', { package: 'pandas' }); await this.pyodide.loadPackage('pandas'); this.installedPackages.add('pandas'); this.emit('package:loaded', { package: 'pandas' }); } } catch (error) { this.emit('package:error', { package: 'pandas', error }); // Don't throw - allow execution to continue } } /** * Check if code requires scipy */ _needsScipy(code) { const scipyPatterns = [ /import\s+scipy/, /from\s+scipy/, /scipy\./ ]; return scipyPatterns.some(pattern => pattern.test(code)); } /** * Ensure scipy is loaded and available */ async _ensureScipyLoaded() { try { if (!this.installedPackages.has('scipy')) { this.emit('package:loading', { package: 'scipy' }); await this.pyodide.loadPackage('scipy'); this.installedPackages.add('scipy'); this.emit('package:loaded', { package: 'scipy' }); } } catch (error) { this.emit('package:error', { package: 'scipy', error }); } } /** * Check if code requires plotly */ _needsPlotly(code) { const plotlyPatterns = [ /import\s+plotly/, /from\s+plotly/, /plotly\./, /import\s+plotly\.graph_objects/, /import\s+plotly\.express/ ]; return plotlyPatterns.some(pattern => pattern.test(code)); } /** * Ensure plotly is loaded and available */ async _ensurePlotlyLoaded() { try { if (!this.installedPackages.has('plotly')) { this.emit('package:loading', { package: 'plotly' }); await this.pyodide.loadPackage('plotly'); this.installedPackages.add('plotly'); this.emit('package:loaded', { package: 'plotly' }); } } catch (error) { this.emit('package:error', { package: 'plotly', error }); } } /** * Check if code requires scikit-learn */ _needsSklearn(code) { const sklearnPatterns = [ /import\s+sklearn/, /from\s+sklearn/, /sklearn\./, /from\s+sklearn\.\w+/ ]; return sklearnPatterns.some(pattern => pattern.test(code)); } /** * Ensure scikit-learn is loaded and available */ async _ensureSklearnLoaded() { try { if (!this.installedPackages.has('scikit-learn')) { this.emit('package:loading', { package: 'scikit-learn' }); await this.pyodide.loadPackage('scikit-learn'); this.installedPackages.add('scikit-learn'); this.emit('package:loaded', { package: 'scikit-learn' }); } } catch (error) { this.emit('package:error', { package: 'scikit-learn', error }); } } _containsDangerousPatterns(code) { const dangerousPatterns = [ /\beval\s*\(/, /\bexec\s*\(/, /\b__import__\s*\(/, /\bsubprocess\./, /\bos\.system\s*\(/, /\bopen\s*\(/, /\bfile\s*\(/ ]; return dangerousPatterns.some(pattern => pattern.test(code)); } } // Custom error for security violations class SecurityError extends Error { constructor(message) { super(message); this.name = 'SecurityError'; } } export default RuntimeManager; export { SecurityError };