File size: 4,248 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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;

  /**
   * Execute a script project.
   * Gathers text files from VFS and sends them to the worker.
   * If already running, aborts the previous execution first.
   */
  async execute(projectId: string, runtime: ScriptRuntime, entryPoint: string): Promise<void> {
    // Abort any running execution
    if (this.running) {
      this.abort();
    }

    // Ensure worker is created
    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();
      };
    }

    // Gather VFS files
    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;
      }
    }

    // Begin error tracking
    beginCompilation();

    this.running = true;
    this.emit({ type: 'status', data: `Running ${runtime === 'python' ? 'Python' : 'Lua'} script...` });

    // Set execution timeout
    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();
        // Emit complete so tool-registry listeners resolve instead of hanging
        this.emit({ type: 'complete', exitCode: 1 });
        this.abort();
      }
    }, EXECUTION_TIMEOUT_MS);

    // Send execution request
    const payload = { runtime, entryPoint, files };
    this.worker.postMessage({ type: 'execute', payload });
  }

  /**
   * Abort the current execution by terminating the worker.
   */
  abort(): void {
    this.clearTimeout();

    if (this.worker) {
      this.worker.terminate();
      this.worker = null;
    }
    this.running = false;
  }

  /**
   * Subscribe to output messages from the worker.
   * Returns an unsubscribe function.
   */
  onOutput(listener: OutputListener): () => void {
    this.listeners.add(listener);
    return () => {
      this.listeners.delete(listener);
    };
  }

  isRunning(): boolean {
    return this.running;
  }

  /**
   * Clean up worker and all listeners.
   */
  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') {
      // Push to compile error pipeline so the orchestrator picks it up
      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();