|
3 | 3 | * SPDX-License-Identifier: MIT |
4 | 4 | */ |
5 | 5 |
|
| 6 | +import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten'; |
6 | 7 | import { |
7 | 8 | CodeNodeSchema, |
8 | 9 | ExecutionContext, |
@@ -48,47 +49,74 @@ export class CodeExecutor implements INodeExecutor { |
48 | 49 | } |
49 | 50 |
|
50 | 51 | private async javascript(inputs: CodeExecutorInputs): Promise<ExecutionResult> { |
51 | | - // Extract script content and inputs |
52 | 52 | const { params = {}, script } = inputs; |
53 | 53 |
|
| 54 | + // Serialize before allocating WASM resources – fails fast on circular references. |
| 55 | + const serializedParams = JSON.stringify(params); |
| 56 | + |
| 57 | + const QuickJS = await getQuickJS(); |
| 58 | + |
| 59 | + // Each execution gets an isolated context; no host globals are exposed by default. |
| 60 | + const context = QuickJS.newContext(); |
54 | 61 | try { |
55 | | - // Create a safe execution environment with basic restrictions |
56 | | - const executeCode = new Function( |
57 | | - 'params', |
58 | | - ` |
59 | | - 'use strict'; |
| 62 | + // Apply resource limits on the underlying runtime. |
| 63 | + const runtime = context.runtime; |
| 64 | + runtime.setMemoryLimit(32 * 1024 * 1024); // 32 MB |
| 65 | + runtime.setMaxStackSize(512 * 1024); // 512 KB |
| 66 | + // Interrupt execution if it runs longer than 1 minute. |
| 67 | + runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + 60_000)); |
60 | 68 |
|
61 | | - ${script.content} |
| 69 | + // Wrap user code: define main, inject params, call main, return result. |
| 70 | + const wrappedCode = ` |
| 71 | +'use strict'; |
62 | 72 |
|
63 | | - // Ensure main function exists |
64 | | - if (typeof main !== 'function') { |
65 | | - throw new Error('main function is required in the script'); |
66 | | - } |
| 73 | +${script.content} |
67 | 74 |
|
68 | | - // Execute main function with params |
69 | | - return main({ params }); |
70 | | - ` |
71 | | - ); |
| 75 | +if (typeof main !== 'function') { |
| 76 | + throw new Error('main function is required in the script'); |
| 77 | +} |
72 | 78 |
|
73 | | - // Execute with timeout protection (1 minute) |
74 | | - const timeoutPromise = new Promise<never>((_, reject) => { |
75 | | - setTimeout(() => { |
76 | | - reject(new Error('Code execution timeout: exceeded 1 minute')); |
77 | | - }, 1000 * 60); |
78 | | - }); |
| 79 | +const __params__ = ${serializedParams}; |
| 80 | +main({ params: __params__ }); |
| 81 | +`; |
79 | 82 |
|
80 | | - // Execute the code with input parameters and timeout |
81 | | - const result = await Promise.race([executeCode(params), timeoutPromise]); |
| 83 | + const evalResult = context.evalCode(wrappedCode); |
| 84 | + const resultHandle = context.unwrapResult(evalResult); |
| 85 | + |
| 86 | + let rawResult: unknown; |
| 87 | + |
| 88 | + try { |
| 89 | + const promiseState = context.getPromiseState(resultHandle); |
| 90 | + if (promiseState.type === 'fulfilled') { |
| 91 | + rawResult = context.dump(promiseState.value); |
| 92 | + promiseState.value.dispose(); |
| 93 | + } else if (promiseState.type === 'rejected') { |
| 94 | + const errMsg = context.dump(promiseState.error); |
| 95 | + promiseState.error.dispose(); |
| 96 | + throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg)); |
| 97 | + } else { |
| 98 | + // Pending promise: resolve asynchronously via the QuickJS event loop. |
| 99 | + const resolvedResult = await context.resolvePromise(resultHandle); |
| 100 | + const resolvedHandle = context.unwrapResult(resolvedResult); |
| 101 | + rawResult = context.dump(resolvedHandle); |
| 102 | + resolvedHandle.dispose(); |
| 103 | + } |
| 104 | + } finally { |
| 105 | + resultHandle.dispose(); |
| 106 | + } |
82 | 107 |
|
83 | | - // Ensure result is an object |
| 108 | + // Ensure result is a plain object. |
84 | 109 | const outputs = |
85 | | - result && typeof result === 'object' && !Array.isArray(result) ? result : { result }; |
| 110 | + rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult) |
| 111 | + ? (rawResult as Record<string, unknown>) |
| 112 | + : { result: rawResult }; |
86 | 113 |
|
87 | | - return { |
88 | | - outputs, |
89 | | - }; |
| 114 | + return { outputs }; |
90 | 115 | } catch (error: any) { |
91 | 116 | throw new Error(`Code execution failed: ${error.message}`); |
| 117 | + } finally { |
| 118 | + // Always release WASM memory for this execution context. |
| 119 | + context.dispose(); |
92 | 120 | } |
93 | 121 | } |
94 | 122 | } |
0 commit comments