Skip to content

Commit a5b7148

Browse files
authored
fix(node): use node subprocess explicitly (#2391)
* fix(node): use node subprocess explicitly * add explicit documentation * fixed build
1 parent 57e6a0b commit a5b7148

File tree

2 files changed

+70
-45
lines changed

2 files changed

+70
-45
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ When running with Docker, use `host.docker.internal` if vLLM is on your host mac
130130

131131
**Requirements:**
132132
- [Bun](https://bun.sh/) runtime
133+
- [Node.js](https://nodejs.org/) v20+ (required for sandboxed code execution)
133134
- PostgreSQL 12+ with [pgvector extension](https://github.com/pgvector/pgvector) (required for AI embeddings)
134135

135136
**Note:** Sim uses vector embeddings for AI features like knowledge bases and semantic search, which requires the `pgvector` PostgreSQL extension.

apps/sim/lib/execution/isolated-vm.ts

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ChildProcess, spawn } from 'node:child_process'
1+
import { type ChildProcess, execSync } from 'node:child_process'
22
import fs from 'node:fs'
33
import path from 'node:path'
44
import { fileURLToPath } from 'node:url'
@@ -7,6 +7,19 @@ import { createLogger } from '@/lib/logs/console/logger'
77

88
const logger = createLogger('IsolatedVMExecution')
99

10+
let nodeAvailable: boolean | null = null
11+
12+
function checkNodeAvailable(): boolean {
13+
if (nodeAvailable !== null) return nodeAvailable
14+
try {
15+
execSync('node --version', { stdio: 'ignore' })
16+
nodeAvailable = true
17+
} catch {
18+
nodeAvailable = false
19+
}
20+
return nodeAvailable
21+
}
22+
1023
export interface IsolatedVMExecutionRequest {
1124
code: string
1225
params: Record<string, unknown>
@@ -171,6 +184,16 @@ async function ensureWorker(): Promise<void> {
171184
if (workerReadyPromise) return workerReadyPromise
172185

173186
workerReadyPromise = new Promise<void>((resolve, reject) => {
187+
if (!checkNodeAvailable()) {
188+
reject(
189+
new Error(
190+
'Node.js is required for code execution but was not found. ' +
191+
'Please install Node.js (v20+) from https://nodejs.org'
192+
)
193+
)
194+
return
195+
}
196+
174197
const currentDir = path.dirname(fileURLToPath(import.meta.url))
175198
const workerPath = path.join(currentDir, 'isolated-vm-worker.cjs')
176199

@@ -179,53 +202,54 @@ async function ensureWorker(): Promise<void> {
179202
return
180203
}
181204

182-
worker = spawn(process.execPath, [workerPath], {
183-
stdio: ['ignore', 'pipe', 'inherit', 'ipc'],
184-
serialization: 'json',
185-
})
205+
import('node:child_process').then(({ spawn }) => {
206+
worker = spawn('node', [workerPath], {
207+
stdio: ['ignore', 'pipe', 'inherit', 'ipc'],
208+
serialization: 'json',
209+
})
186210

187-
worker.on('message', handleWorkerMessage)
188-
189-
const startTimeout = setTimeout(() => {
190-
worker?.kill()
191-
worker = null
192-
workerReady = false
193-
workerReadyPromise = null
194-
reject(new Error('Worker failed to start within timeout'))
195-
}, 10000)
196-
197-
const readyHandler = (message: unknown) => {
198-
if (
199-
typeof message === 'object' &&
200-
message !== null &&
201-
(message as { type?: string }).type === 'ready'
202-
) {
203-
workerReady = true
204-
clearTimeout(startTimeout)
205-
worker?.off('message', readyHandler)
206-
resolve()
207-
}
208-
}
209-
worker.on('message', readyHandler)
211+
worker.on('message', handleWorkerMessage)
210212

211-
worker.on('exit', (code) => {
212-
logger.warn('Isolated-vm worker exited', { code })
213-
if (workerIdleTimeout) {
214-
clearTimeout(workerIdleTimeout)
215-
workerIdleTimeout = null
216-
}
217-
worker = null
218-
workerReady = false
219-
workerReadyPromise = null
220-
for (const [id, pending] of pendingExecutions) {
221-
clearTimeout(pending.timeout)
222-
pending.resolve({
223-
result: null,
224-
stdout: '',
225-
error: { message: 'Worker process exited unexpectedly', name: 'WorkerError' },
226-
})
227-
pendingExecutions.delete(id)
213+
const startTimeout = setTimeout(() => {
214+
worker?.kill()
215+
worker = null
216+
workerReady = false
217+
workerReadyPromise = null
218+
reject(new Error('Worker failed to start within timeout'))
219+
}, 10000)
220+
221+
const readyHandler = (message: unknown) => {
222+
if (
223+
typeof message === 'object' &&
224+
message !== null &&
225+
(message as { type?: string }).type === 'ready'
226+
) {
227+
workerReady = true
228+
clearTimeout(startTimeout)
229+
worker?.off('message', readyHandler)
230+
resolve()
231+
}
228232
}
233+
worker.on('message', readyHandler)
234+
235+
worker.on('exit', () => {
236+
if (workerIdleTimeout) {
237+
clearTimeout(workerIdleTimeout)
238+
workerIdleTimeout = null
239+
}
240+
worker = null
241+
workerReady = false
242+
workerReadyPromise = null
243+
for (const [id, pending] of pendingExecutions) {
244+
clearTimeout(pending.timeout)
245+
pending.resolve({
246+
result: null,
247+
stdout: '',
248+
error: { message: 'Worker process exited unexpectedly', name: 'WorkerError' },
249+
})
250+
pendingExecutions.delete(id)
251+
}
252+
})
229253
})
230254
})
231255

0 commit comments

Comments
 (0)