mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(code): additional cleanup for stdout response from WebContainer
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { executeCode, getWebContainer } from '@/lib/webcontainer'
|
||||
|
||||
// Create a context for WebContainer state and utilities
|
||||
interface WebContainerContextType {
|
||||
isReady: boolean
|
||||
error: string | null
|
||||
executeFunction: (code: string, params?: Record<string, any>) => Promise<any>
|
||||
}
|
||||
|
||||
const WebContainerContext = createContext<WebContainerContextType>({
|
||||
isReady: false,
|
||||
error: null,
|
||||
executeFunction: async () => ({ success: false, error: 'WebContainer not initialized' }),
|
||||
})
|
||||
|
||||
// Hook to use WebContainer
|
||||
export const useWebContainer = () => useContext(WebContainerContext)
|
||||
|
||||
/**
|
||||
* WebContainerProvider initializes the WebContainer API on the client side
|
||||
* This component should be included near the root of the application
|
||||
*/
|
||||
export function WebContainerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isIsolated, setIsIsolated] = useState<boolean | null>(null)
|
||||
|
||||
// Function to execute code directly from client components
|
||||
const executeFunction = useCallback(
|
||||
async (code: string, params: Record<string, any> = {}) => {
|
||||
if (!isInitialized) {
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: 'WebContainer not initialized',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await executeCode(code, params)
|
||||
} catch (err: any) {
|
||||
console.error('WebContainer execution error:', err)
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: err.message || 'Error executing function',
|
||||
}
|
||||
}
|
||||
},
|
||||
[isInitialized]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we're in a cross-origin isolated context
|
||||
if (typeof window !== 'undefined') {
|
||||
setIsIsolated(!!window.crossOriginIsolated)
|
||||
}
|
||||
|
||||
const initWebContainer = async () => {
|
||||
try {
|
||||
// Initialize WebContainer when the component mounts
|
||||
await getWebContainer()
|
||||
setIsInitialized(true)
|
||||
} catch (err: any) {
|
||||
console.error('Failed to initialize WebContainer:', err)
|
||||
setError(err.message || 'Failed to initialize WebContainer')
|
||||
}
|
||||
}
|
||||
|
||||
// Only try to initialize if we're properly isolated
|
||||
if (isIsolated) {
|
||||
initWebContainer()
|
||||
}
|
||||
}, [isIsolated])
|
||||
|
||||
// Provide WebContainer context
|
||||
const contextValue = {
|
||||
isReady: isInitialized,
|
||||
error,
|
||||
executeFunction,
|
||||
}
|
||||
|
||||
if (error || isIsolated === false) {
|
||||
const needsRestart = error?.includes('restart') || !isIsolated
|
||||
|
||||
return (
|
||||
<WebContainerContext.Provider value={contextValue}>
|
||||
<div className="fixed bottom-4 right-4 max-w-md bg-destructive/90 text-destructive-foreground p-4 rounded-md shadow-lg z-50">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-5 w-5 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">WebContainer Error</h3>
|
||||
<p className="text-sm">
|
||||
{isIsolated === false
|
||||
? 'Cross-Origin Isolation is not enabled. WebContainers require COOP/COEP headers.'
|
||||
: error}
|
||||
</p>
|
||||
{needsRestart && (
|
||||
<p className="text-xs mt-2 font-medium">
|
||||
Please restart the server for the COOP/COEP headers to take effect.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs mt-2">
|
||||
Note: Function blocks will fall back to less secure VM-based execution until this is
|
||||
resolved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</WebContainerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<WebContainerContext.Provider value={contextValue}>{children}</WebContainerContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -482,7 +482,12 @@ export class FunctionBlockHandler implements BlockHandler {
|
||||
|
||||
return { response: result.output }
|
||||
} catch (error: any) {
|
||||
console.warn('WebContainer execution failed, falling back to VM:', error.message)
|
||||
console.warn('WebContainer execution failed, falling back to VM:', error)
|
||||
console.error('WebContainer error details:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+57
-21
@@ -172,6 +172,8 @@ export async function executeCode(
|
||||
}> {
|
||||
const startTime = Date.now()
|
||||
let process: any = null
|
||||
let stdout = ''
|
||||
let result: any = null
|
||||
|
||||
try {
|
||||
// Get or initialize WebContainer
|
||||
@@ -186,10 +188,22 @@ export async function executeCode(
|
||||
const imports = []
|
||||
let remainingCode = code
|
||||
|
||||
// More carefully extract imports to prevent consuming the entire code
|
||||
let match
|
||||
while ((match = importRegex.exec(code)) !== null) {
|
||||
imports.push(match[0])
|
||||
remainingCode = remainingCode.replace(match[0], '')
|
||||
let codeLines = code.split('\n')
|
||||
let importLines = []
|
||||
|
||||
// Find import lines
|
||||
for (let i = 0; i < codeLines.length; i++) {
|
||||
if (codeLines[i].trim().startsWith('import ')) {
|
||||
importLines.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract imports and remaining code separately
|
||||
if (importLines.length > 0) {
|
||||
imports.push(...importLines.map((idx) => codeLines[idx]))
|
||||
remainingCode = codeLines.filter((_, idx) => !importLines.includes(idx)).join('\n')
|
||||
}
|
||||
|
||||
// Create the module file with proper structure
|
||||
@@ -222,8 +236,6 @@ export async function executeCode(
|
||||
await webcontainer.fs.writeFile('/code.mjs', moduleCode)
|
||||
|
||||
// Set up stdout capture
|
||||
let stdout = ''
|
||||
let result: any = null
|
||||
let processCompleted = false
|
||||
|
||||
// Run the code with Node.js in ES module mode
|
||||
@@ -258,7 +270,7 @@ export async function executeCode(
|
||||
})
|
||||
)
|
||||
|
||||
// Listen for process exit
|
||||
// Handle process exit
|
||||
process.exit.then((code: number) => {
|
||||
processCompleted = true
|
||||
if (code === 0) {
|
||||
@@ -269,25 +281,36 @@ export async function executeCode(
|
||||
})
|
||||
})
|
||||
|
||||
// Add a timeout
|
||||
// Set up error handling
|
||||
const errorPromise = new Promise<void>((_, reject) => {
|
||||
process.stderr.pipeTo(
|
||||
new WritableStream({
|
||||
write(data) {
|
||||
console.error('WebContainer executeCode - Process error:', data)
|
||||
stdout += `ERROR: ${data}\n`
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// Set up timeout
|
||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
if (!processCompleted && process) {
|
||||
process.kill()
|
||||
if (!processCompleted) {
|
||||
console.error('WebContainer executeCode - Process timed out after', timeout, 'ms')
|
||||
reject(new Error(`Execution timed out after ${timeout}ms`))
|
||||
}
|
||||
reject(new Error(`Execution timed out after ${timeout}ms`))
|
||||
}, timeout)
|
||||
})
|
||||
|
||||
// Wait for either completion or timeout
|
||||
await Promise.race([resultPromise, timeoutPromise])
|
||||
await Promise.race([resultPromise, errorPromise, timeoutPromise])
|
||||
|
||||
const executionTime = Date.now() - startTime
|
||||
|
||||
// Clean stdout of our result markers
|
||||
// Clean the stdout from our internal markers and result JSON
|
||||
const cleanedStdout = stdout
|
||||
.replace(/(__RESULT_START__|__RESULT_END__)/g, '')
|
||||
.replace(/\n{3,}/g, '\n\n') // Reduce multiple newlines
|
||||
.replace(/\r?\n?__RESULT_START__\r?\n?[\s\S]*?__RESULT_END__\r?\n?/g, '')
|
||||
.trim()
|
||||
|
||||
return {
|
||||
@@ -299,25 +322,38 @@ export async function executeCode(
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Ensure process is killed on error
|
||||
console.error('WebContainer executeCode - Execution failed:', {
|
||||
error: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
stdout: stdout || 'No stdout',
|
||||
})
|
||||
|
||||
// Try to kill the process if it's still running
|
||||
if (process) {
|
||||
try {
|
||||
process.kill()
|
||||
await process.kill()
|
||||
} catch (killError) {
|
||||
console.error('Error killing process:', killError)
|
||||
console.error('WebContainer executeCode - Failed to kill process:', killError)
|
||||
}
|
||||
}
|
||||
|
||||
const executionTime = Date.now() - startTime
|
||||
// Clean stdout before returning, even in error case
|
||||
let cleanedStdout = ''
|
||||
if (stdout) {
|
||||
cleanedStdout = stdout
|
||||
.replace(/\r?\n?__RESULT_START__\r?\n?[\s\S]*?__RESULT_END__\r?\n?/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Unknown error occurred during execution',
|
||||
output: {
|
||||
result: null,
|
||||
stdout: '',
|
||||
executionTime,
|
||||
stdout: cleanedStdout,
|
||||
executionTime: Date.now() - startTime,
|
||||
},
|
||||
error: error.message || 'Code execution failed',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user