stone-lang 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- package/packages/semver.js +635 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessManager - Manages Stone script execution processes
|
|
3
|
+
*
|
|
4
|
+
* Tracks running scripts, their owners, and provides lifecycle management.
|
|
5
|
+
* When a client disconnects, all its owned processes are killed.
|
|
6
|
+
*
|
|
7
|
+
* Process states:
|
|
8
|
+
* - pending: Waiting to start
|
|
9
|
+
* - running: Currently executing
|
|
10
|
+
* - completed: Finished successfully
|
|
11
|
+
* - failed: Finished with error
|
|
12
|
+
* - killed: Terminated by user
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Process entry in the process table
|
|
19
|
+
*/
|
|
20
|
+
class StoneProcess {
|
|
21
|
+
constructor(id, script, owner, options = {}) {
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.script = script; // Script path or source
|
|
24
|
+
this.owner = owner; // Client ID that started this process
|
|
25
|
+
this.status = 'pending';
|
|
26
|
+
this.startTime = null;
|
|
27
|
+
this.endTime = null;
|
|
28
|
+
this.result = null;
|
|
29
|
+
this.error = null;
|
|
30
|
+
this.terminalsData = {};
|
|
31
|
+
|
|
32
|
+
// Execution options
|
|
33
|
+
this.cwd = options.cwd || process.cwd();
|
|
34
|
+
this.variables = options.variables || {};
|
|
35
|
+
this.maxIterations = options.maxIterations || 100000;
|
|
36
|
+
|
|
37
|
+
// Runner reference (set when execution starts)
|
|
38
|
+
this.runner = null;
|
|
39
|
+
|
|
40
|
+
// Kill signal
|
|
41
|
+
this._killRequested = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toJSON() {
|
|
45
|
+
return {
|
|
46
|
+
id: this.id,
|
|
47
|
+
script: this.script,
|
|
48
|
+
owner: this.owner,
|
|
49
|
+
status: this.status,
|
|
50
|
+
startTime: this.startTime,
|
|
51
|
+
endTime: this.endTime,
|
|
52
|
+
duration: this.getDuration(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getDuration() {
|
|
57
|
+
if (!this.startTime) return 0;
|
|
58
|
+
const end = this.endTime || Date.now();
|
|
59
|
+
return end - this.startTime;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
requestKill() {
|
|
63
|
+
this._killRequested = true;
|
|
64
|
+
if (this.runner && typeof this.runner.kill === 'function') {
|
|
65
|
+
this.runner.kill();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isKillRequested() {
|
|
70
|
+
return this._killRequested;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ProcessManager - Central process table and lifecycle management
|
|
76
|
+
*/
|
|
77
|
+
export class ProcessManager extends EventEmitter {
|
|
78
|
+
constructor() {
|
|
79
|
+
super();
|
|
80
|
+
this.processes = new Map(); // processId -> StoneProcess
|
|
81
|
+
this.processCounter = 0;
|
|
82
|
+
|
|
83
|
+
// Owner tracking: clientId -> Set of processIds
|
|
84
|
+
this.ownerProcesses = new Map();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a new process entry
|
|
89
|
+
* @param {string} script - Script path or source
|
|
90
|
+
* @param {string} owner - Client ID that owns this process
|
|
91
|
+
* @param {Object} options - Execution options
|
|
92
|
+
* @returns {StoneProcess} The created process
|
|
93
|
+
*/
|
|
94
|
+
createProcess(script, owner, options = {}) {
|
|
95
|
+
const id = `p${++this.processCounter}`;
|
|
96
|
+
const process = new StoneProcess(id, script, owner, options);
|
|
97
|
+
this.processes.set(id, process);
|
|
98
|
+
|
|
99
|
+
// Track owner
|
|
100
|
+
if (!this.ownerProcesses.has(owner)) {
|
|
101
|
+
this.ownerProcesses.set(owner, new Set());
|
|
102
|
+
}
|
|
103
|
+
this.ownerProcesses.get(owner).add(id);
|
|
104
|
+
|
|
105
|
+
this.emit('process:created', process);
|
|
106
|
+
return process;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Start a process
|
|
111
|
+
* @param {string} processId - Process ID
|
|
112
|
+
*/
|
|
113
|
+
startProcess(processId) {
|
|
114
|
+
const process = this.processes.get(processId);
|
|
115
|
+
if (!process) {
|
|
116
|
+
throw new Error(`Process not found: ${processId}`);
|
|
117
|
+
}
|
|
118
|
+
if (process.status !== 'pending') {
|
|
119
|
+
throw new Error(`Process ${processId} is not in pending state`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.status = 'running';
|
|
123
|
+
process.startTime = Date.now();
|
|
124
|
+
this.emit('process:started', process);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Complete a process successfully
|
|
129
|
+
* @param {string} processId - Process ID
|
|
130
|
+
* @param {any} result - Execution result
|
|
131
|
+
* @param {Object} terminalsData - Terminal data
|
|
132
|
+
*/
|
|
133
|
+
completeProcess(processId, result, terminalsData = {}) {
|
|
134
|
+
const process = this.processes.get(processId);
|
|
135
|
+
if (!process) return;
|
|
136
|
+
|
|
137
|
+
process.status = 'completed';
|
|
138
|
+
process.endTime = Date.now();
|
|
139
|
+
process.result = result;
|
|
140
|
+
process.terminalsData = terminalsData;
|
|
141
|
+
|
|
142
|
+
this.emit('process:completed', process);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fail a process
|
|
147
|
+
* @param {string} processId - Process ID
|
|
148
|
+
* @param {Error|string} error - Error that caused failure
|
|
149
|
+
*/
|
|
150
|
+
failProcess(processId, error) {
|
|
151
|
+
const process = this.processes.get(processId);
|
|
152
|
+
if (!process) return;
|
|
153
|
+
|
|
154
|
+
process.status = 'failed';
|
|
155
|
+
process.endTime = Date.now();
|
|
156
|
+
process.error = error instanceof Error ? error.message : error;
|
|
157
|
+
|
|
158
|
+
this.emit('process:failed', process);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Kill a process
|
|
163
|
+
* @param {string} processId - Process ID
|
|
164
|
+
* @param {string} requestedBy - Client ID requesting the kill (for auth)
|
|
165
|
+
* @returns {boolean} Whether the kill was successful
|
|
166
|
+
*/
|
|
167
|
+
killProcess(processId, requestedBy = null) {
|
|
168
|
+
const process = this.processes.get(processId);
|
|
169
|
+
if (!process) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check authorization (only owner can kill, unless admin)
|
|
174
|
+
if (requestedBy && process.owner !== requestedBy) {
|
|
175
|
+
throw new Error(`Not authorized to kill process ${processId}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Only kill running processes
|
|
179
|
+
if (process.status !== 'running') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.requestKill();
|
|
184
|
+
process.status = 'killed';
|
|
185
|
+
process.endTime = Date.now();
|
|
186
|
+
|
|
187
|
+
this.emit('process:killed', process);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get a process by ID
|
|
193
|
+
* @param {string} processId - Process ID
|
|
194
|
+
* @returns {StoneProcess|null}
|
|
195
|
+
*/
|
|
196
|
+
getProcess(processId) {
|
|
197
|
+
return this.processes.get(processId) || null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* List all processes
|
|
202
|
+
* @param {Object} filter - Optional filter { status, owner }
|
|
203
|
+
* @returns {StoneProcess[]}
|
|
204
|
+
*/
|
|
205
|
+
listProcesses(filter = {}) {
|
|
206
|
+
let result = Array.from(this.processes.values());
|
|
207
|
+
|
|
208
|
+
if (filter.status) {
|
|
209
|
+
result = result.filter(p => p.status === filter.status);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (filter.owner) {
|
|
213
|
+
result = result.filter(p => p.owner === filter.owner);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get processes owned by a client
|
|
221
|
+
* @param {string} owner - Client ID
|
|
222
|
+
* @returns {StoneProcess[]}
|
|
223
|
+
*/
|
|
224
|
+
getProcessesByOwner(owner) {
|
|
225
|
+
const processIds = this.ownerProcesses.get(owner) || new Set();
|
|
226
|
+
return Array.from(processIds).map(id => this.processes.get(id)).filter(Boolean);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Kill all processes owned by a client
|
|
231
|
+
* Called when a client disconnects
|
|
232
|
+
* @param {string} owner - Client ID
|
|
233
|
+
* @returns {string[]} IDs of killed processes
|
|
234
|
+
*/
|
|
235
|
+
killProcessesByOwner(owner) {
|
|
236
|
+
const killed = [];
|
|
237
|
+
const processIds = this.ownerProcesses.get(owner) || new Set();
|
|
238
|
+
|
|
239
|
+
for (const processId of processIds) {
|
|
240
|
+
const process = this.processes.get(processId);
|
|
241
|
+
if (process && process.status === 'running') {
|
|
242
|
+
this.killProcess(processId);
|
|
243
|
+
killed.push(processId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return killed;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Clean up a process entry
|
|
252
|
+
* Called after process completes/fails and client has retrieved result
|
|
253
|
+
* @param {string} processId - Process ID
|
|
254
|
+
*/
|
|
255
|
+
removeProcess(processId) {
|
|
256
|
+
const process = this.processes.get(processId);
|
|
257
|
+
if (!process) return;
|
|
258
|
+
|
|
259
|
+
// Remove from owner tracking
|
|
260
|
+
const ownerSet = this.ownerProcesses.get(process.owner);
|
|
261
|
+
if (ownerSet) {
|
|
262
|
+
ownerSet.delete(processId);
|
|
263
|
+
if (ownerSet.size === 0) {
|
|
264
|
+
this.ownerProcesses.delete(process.owner);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.processes.delete(processId);
|
|
269
|
+
this.emit('process:removed', process);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Clean up old completed/failed processes
|
|
274
|
+
* @param {number} maxAge - Max age in ms (default 1 hour)
|
|
275
|
+
*/
|
|
276
|
+
cleanupOldProcesses(maxAge = 3600000) {
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const toRemove = [];
|
|
279
|
+
|
|
280
|
+
for (const [id, process] of this.processes.entries()) {
|
|
281
|
+
if (process.status === 'completed' ||
|
|
282
|
+
process.status === 'failed' ||
|
|
283
|
+
process.status === 'killed') {
|
|
284
|
+
if (process.endTime && (now - process.endTime) > maxAge) {
|
|
285
|
+
toRemove.push(id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const id of toRemove) {
|
|
291
|
+
this.removeProcess(id);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return toRemove.length;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get process count by status
|
|
299
|
+
* @returns {Object} { pending, running, completed, failed, killed }
|
|
300
|
+
*/
|
|
301
|
+
getProcessStats() {
|
|
302
|
+
const stats = { pending: 0, running: 0, completed: 0, failed: 0, killed: 0, total: 0 };
|
|
303
|
+
|
|
304
|
+
for (const process of this.processes.values()) {
|
|
305
|
+
stats[process.status] = (stats[process.status] || 0) + 1;
|
|
306
|
+
stats.total++;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return stats;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if there are any running processes
|
|
314
|
+
* @returns {boolean}
|
|
315
|
+
*/
|
|
316
|
+
hasRunningProcesses() {
|
|
317
|
+
for (const process of this.processes.values()) {
|
|
318
|
+
if (process.status === 'running') {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export { StoneProcess };
|
|
327
|
+
export default ProcessManager;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessRunner - Executes Stone scripts and streams events
|
|
3
|
+
*
|
|
4
|
+
* Uses the StoneEngine with a streaming OutputAdapter to send
|
|
5
|
+
* real-time updates to connected clients via the IPC server.
|
|
6
|
+
*
|
|
7
|
+
* Events emitted:
|
|
8
|
+
* - terminal:create - New terminal created
|
|
9
|
+
* - terminal:add - Data added to terminal
|
|
10
|
+
* - terminal:set - Terminal data replaced
|
|
11
|
+
* - terminal:clear - Terminal cleared
|
|
12
|
+
* - print - Print output
|
|
13
|
+
* - error - Execution error
|
|
14
|
+
* - complete - Execution completed
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
import { OutputAdapter } from '../adapters/OutputAdapter.js';
|
|
19
|
+
import { StoneEngine } from '../StoneEngine.js';
|
|
20
|
+
import { createNodePackageContext } from '../packages/packageResolver.js';
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* StreamingOutputAdapter - Streams output events to the process runner
|
|
26
|
+
*/
|
|
27
|
+
class StreamingOutputAdapter extends OutputAdapter {
|
|
28
|
+
constructor(processId, eventEmitter) {
|
|
29
|
+
super();
|
|
30
|
+
this.processId = processId;
|
|
31
|
+
this.emitter = eventEmitter;
|
|
32
|
+
this.terminalsData = {}; // Local copy for backward compat
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
print(text) {
|
|
36
|
+
this.emitter.emit('event', {
|
|
37
|
+
event: 'print',
|
|
38
|
+
processId: this.processId,
|
|
39
|
+
value: text,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createTerminal(id, type, config) {
|
|
44
|
+
this.terminalsData[id] = {
|
|
45
|
+
type,
|
|
46
|
+
config,
|
|
47
|
+
plots: type === 'graph2d' ? [] : undefined,
|
|
48
|
+
objects: type === 'graph3d' ? [] : undefined,
|
|
49
|
+
lines: type === 'console' ? [] : undefined,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this.emitter.emit('event', {
|
|
53
|
+
event: 'terminal:create',
|
|
54
|
+
processId: this.processId,
|
|
55
|
+
id,
|
|
56
|
+
type,
|
|
57
|
+
terminalType: type, // Alias for renderer compatibility
|
|
58
|
+
config,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
addToTerminal(id, data) {
|
|
63
|
+
const terminal = this.terminalsData[id];
|
|
64
|
+
if (terminal) {
|
|
65
|
+
const items = Array.isArray(data) ? data : [data];
|
|
66
|
+
switch (terminal.type) {
|
|
67
|
+
case 'graph2d':
|
|
68
|
+
terminal.plots.push(...items);
|
|
69
|
+
break;
|
|
70
|
+
case 'graph3d':
|
|
71
|
+
terminal.objects.push(...items);
|
|
72
|
+
break;
|
|
73
|
+
case 'console':
|
|
74
|
+
terminal.lines.push(...items);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.emitter.emit('event', {
|
|
80
|
+
event: 'terminal:add',
|
|
81
|
+
processId: this.processId,
|
|
82
|
+
id,
|
|
83
|
+
data,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setTerminal(id, data) {
|
|
88
|
+
const terminal = this.terminalsData[id];
|
|
89
|
+
if (terminal) {
|
|
90
|
+
const items = Array.isArray(data) ? data : [data];
|
|
91
|
+
switch (terminal.type) {
|
|
92
|
+
case 'graph2d':
|
|
93
|
+
terminal.plots = items;
|
|
94
|
+
break;
|
|
95
|
+
case 'graph3d':
|
|
96
|
+
terminal.objects = items;
|
|
97
|
+
break;
|
|
98
|
+
case 'console':
|
|
99
|
+
terminal.lines = items;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.emitter.emit('event', {
|
|
105
|
+
event: 'terminal:set',
|
|
106
|
+
processId: this.processId,
|
|
107
|
+
id,
|
|
108
|
+
data,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
clearTerminal(id) {
|
|
113
|
+
const terminal = this.terminalsData[id];
|
|
114
|
+
if (terminal) {
|
|
115
|
+
switch (terminal.type) {
|
|
116
|
+
case 'graph2d':
|
|
117
|
+
terminal.plots = [];
|
|
118
|
+
break;
|
|
119
|
+
case 'graph3d':
|
|
120
|
+
terminal.objects = [];
|
|
121
|
+
break;
|
|
122
|
+
case 'console':
|
|
123
|
+
terminal.lines = [];
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.emitter.emit('event', {
|
|
129
|
+
event: 'terminal:clear',
|
|
130
|
+
processId: this.processId,
|
|
131
|
+
id,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
error(message, location) {
|
|
136
|
+
this.emitter.emit('event', {
|
|
137
|
+
event: 'error',
|
|
138
|
+
processId: this.processId,
|
|
139
|
+
message,
|
|
140
|
+
location,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
finish(success, result) {
|
|
145
|
+
// Don't emit here - ProcessRunner handles completion
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getTerminalsData() {
|
|
149
|
+
return this.terminalsData;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* ProcessRunner - Runs a Stone script and streams output
|
|
155
|
+
*/
|
|
156
|
+
export class ProcessRunner extends EventEmitter {
|
|
157
|
+
constructor(stoneProcess, options = {}) {
|
|
158
|
+
super();
|
|
159
|
+
this.process = stoneProcess;
|
|
160
|
+
this.engine = null;
|
|
161
|
+
this.adapter = null;
|
|
162
|
+
this._killed = false;
|
|
163
|
+
|
|
164
|
+
// Options
|
|
165
|
+
this.scriptPath = options.scriptPath || null;
|
|
166
|
+
this.source = options.source || null;
|
|
167
|
+
this.cwd = options.cwd || process.cwd();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Run the script
|
|
172
|
+
* @returns {Promise<Object>} Execution result
|
|
173
|
+
*/
|
|
174
|
+
async run() {
|
|
175
|
+
if (this._killed) {
|
|
176
|
+
return { success: false, error: 'Process was killed before starting' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Create streaming adapter
|
|
181
|
+
this.adapter = new StreamingOutputAdapter(this.process.id, this);
|
|
182
|
+
|
|
183
|
+
// Create engine with adapter
|
|
184
|
+
this.engine = new StoneEngine({
|
|
185
|
+
outputAdapter: this.adapter,
|
|
186
|
+
debug: false,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Set up package context for stone_modules resolution
|
|
190
|
+
try {
|
|
191
|
+
const packageContext = await createNodePackageContext();
|
|
192
|
+
this.engine.setPackageContext(packageContext);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// Package context not available, continue without it
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Load source code
|
|
198
|
+
let source = this.source;
|
|
199
|
+
let filename = '<stdin>';
|
|
200
|
+
|
|
201
|
+
if (!source && this.scriptPath) {
|
|
202
|
+
// Read from file
|
|
203
|
+
const fullPath = path.isAbsolute(this.scriptPath)
|
|
204
|
+
? this.scriptPath
|
|
205
|
+
: path.join(this.cwd, this.scriptPath);
|
|
206
|
+
|
|
207
|
+
if (!fs.existsSync(fullPath)) {
|
|
208
|
+
throw new Error(`Script not found: ${fullPath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
source = fs.readFileSync(fullPath, 'utf8');
|
|
212
|
+
filename = this.scriptPath;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!source) {
|
|
216
|
+
throw new Error('No script source provided');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Emit start event
|
|
220
|
+
this.emit('event', {
|
|
221
|
+
event: 'process:start',
|
|
222
|
+
processId: this.process.id,
|
|
223
|
+
script: filename,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Check for kill before expensive operations
|
|
227
|
+
if (this._killed) {
|
|
228
|
+
return { success: false, error: 'Process was killed' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Execute
|
|
232
|
+
const result = await this.engine.execute(source, {
|
|
233
|
+
filename,
|
|
234
|
+
currentPath: this.scriptPath ? path.dirname(path.resolve(this.cwd, this.scriptPath)) : this.cwd,
|
|
235
|
+
variables: this.process.variables || {},
|
|
236
|
+
maxIterations: this.process.maxIterations || 100000,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Emit completion
|
|
240
|
+
this.emit('event', {
|
|
241
|
+
event: 'process:end',
|
|
242
|
+
processId: this.process.id,
|
|
243
|
+
success: result.success,
|
|
244
|
+
result: result.result,
|
|
245
|
+
error: result.error || null,
|
|
246
|
+
duration: result.duration,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: result.success,
|
|
251
|
+
result: result.result,
|
|
252
|
+
error: result.error,
|
|
253
|
+
terminalsData: this.adapter.getTerminalsData(),
|
|
254
|
+
duration: result.duration,
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// Emit error event
|
|
258
|
+
this.emit('event', {
|
|
259
|
+
event: 'process:end',
|
|
260
|
+
processId: this.process.id,
|
|
261
|
+
success: false,
|
|
262
|
+
error: error.message,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
error: error.message,
|
|
268
|
+
terminalsData: this.adapter ? this.adapter.getTerminalsData() : {},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Kill the running script
|
|
275
|
+
*/
|
|
276
|
+
kill() {
|
|
277
|
+
this._killed = true;
|
|
278
|
+
// The VM/executor will check for kill flag
|
|
279
|
+
// In the future, could use AbortController pattern
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if process was killed
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
isKilled() {
|
|
287
|
+
return this._killed;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Create and run a process
|
|
293
|
+
* @param {StoneProcess} stoneProcess - Process entry from ProcessManager
|
|
294
|
+
* @param {Object} options - Runner options
|
|
295
|
+
* @returns {Promise<Object>} Execution result
|
|
296
|
+
*/
|
|
297
|
+
export async function runProcess(stoneProcess, options = {}) {
|
|
298
|
+
const runner = new ProcessRunner(stoneProcess, options);
|
|
299
|
+
|
|
300
|
+
// Link runner to process for kill support
|
|
301
|
+
stoneProcess.runner = runner;
|
|
302
|
+
|
|
303
|
+
return runner.run();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { StreamingOutputAdapter };
|
|
307
|
+
export default ProcessRunner;
|