| import { vfs } from '@/lib/vfs'; |
| import { logger } from '@/lib/utils'; |
| import { beginCompilation, pushCompileError, commitCompilation } from '@/lib/preview/compile-errors'; |
| import type { ScriptRuntime, ScriptWorkerResponse } from './types'; |
|
|
| type OutputListener = (msg: ScriptWorkerResponse) => void; |
|
|
| const EXECUTION_TIMEOUT_MS = 60_000; |
|
|
| class ScriptRunner { |
| private worker: Worker | null = null; |
| private running = false; |
| private listeners = new Set<OutputListener>(); |
| private timeoutId: ReturnType<typeof setTimeout> | null = null; |
|
|
| |
| |
| |
| |
| |
| async execute(projectId: string, runtime: ScriptRuntime, entryPoint: string): Promise<void> { |
| |
| if (this.running) { |
| this.abort(); |
| } |
|
|
| |
| if (!this.worker) { |
| try { |
| this.worker = new Worker('/workers/script-worker.js'); |
| } catch (err) { |
| this.emit({ type: 'error', data: 'Failed to create worker: ' + String(err) }); |
| return; |
| } |
| this.worker.onmessage = (event: MessageEvent<ScriptWorkerResponse>) => { |
| this.handleWorkerMessage(event.data); |
| }; |
| this.worker.onerror = (event) => { |
| this.emit({ type: 'error', data: 'Worker error: ' + (event.message || 'Unknown error') }); |
| this.running = false; |
| this.clearTimeout(); |
| }; |
| } |
|
|
| |
| await vfs.init(); |
| const allFiles = await vfs.listFiles(projectId); |
| const files: Record<string, string> = {}; |
|
|
| for (const file of allFiles) { |
| if (typeof file.content === 'string') { |
| files[file.path] = file.content; |
| } |
| } |
|
|
| |
| beginCompilation(); |
|
|
| this.running = true; |
| this.emit({ type: 'status', data: `Running ${runtime === 'python' ? 'Python' : 'Lua'} script...` }); |
|
|
| |
| this.timeoutId = setTimeout(() => { |
| if (this.running) { |
| this.emit({ type: 'error', data: `Script execution timed out after ${EXECUTION_TIMEOUT_MS / 1000} seconds` }); |
| pushCompileError(entryPoint, `Execution timed out after ${EXECUTION_TIMEOUT_MS / 1000}s — possible infinite loop`); |
| commitCompilation(); |
| |
| this.emit({ type: 'complete', exitCode: 1 }); |
| this.abort(); |
| } |
| }, EXECUTION_TIMEOUT_MS); |
|
|
| |
| const payload = { runtime, entryPoint, files }; |
| this.worker.postMessage({ type: 'execute', payload }); |
| } |
|
|
| |
| |
| |
| abort(): void { |
| this.clearTimeout(); |
|
|
| if (this.worker) { |
| this.worker.terminate(); |
| this.worker = null; |
| } |
| this.running = false; |
| } |
|
|
| |
| |
| |
| |
| onOutput(listener: OutputListener): () => void { |
| this.listeners.add(listener); |
| return () => { |
| this.listeners.delete(listener); |
| }; |
| } |
|
|
| isRunning(): boolean { |
| return this.running; |
| } |
|
|
| |
| |
| |
| dispose(): void { |
| this.abort(); |
| this.listeners.clear(); |
| } |
|
|
| private handleWorkerMessage(msg: ScriptWorkerResponse): void { |
| if (msg.type === 'complete') { |
| this.running = false; |
| this.clearTimeout(); |
| commitCompilation(); |
| } |
|
|
| if (msg.type === 'error' || msg.type === 'stderr') { |
| |
| const errorData = msg.data; |
| if (errorData && errorData.trim()) { |
| pushCompileError('script', errorData); |
| } |
| } |
|
|
| this.emit(msg); |
| } |
|
|
| private emit(msg: ScriptWorkerResponse): void { |
| for (const listener of this.listeners) { |
| try { |
| listener(msg); |
| } catch (err) { |
| logger.error('Script output listener error:', err); |
| } |
| } |
| } |
|
|
| private clearTimeout(): void { |
| if (this.timeoutId) { |
| clearTimeout(this.timeoutId); |
| this.timeoutId = null; |
| } |
| } |
| } |
|
|
| export const scriptRunner = new ScriptRunner(); |
|
|