mirror of https://github.com/nocodb/nocodb
DarkPhoenix2704
2 months ago
9 changed files with 632 additions and 0 deletions
@ -0,0 +1,152 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Secure Script Executor</title> |
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs/loader.js"></script> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> |
||||||
|
<style> |
||||||
|
body { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
height: 100vh; |
||||||
|
font-family: 'Arial', sans-serif; |
||||||
|
} |
||||||
|
#editor, #output { |
||||||
|
height: calc(100vh - 7rem); |
||||||
|
} |
||||||
|
.console-line { |
||||||
|
padding: 2px 5px; |
||||||
|
border-bottom: 1px solid #e2e8f0; |
||||||
|
} |
||||||
|
.log { color: #2d3748; } |
||||||
|
.error { color: #e53e3e; } |
||||||
|
.warn { color: #d69e2e; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="bg-gray-100"> |
||||||
|
<nav class="bg-blue-600 text-white p-4 flex justify-between items-center"> |
||||||
|
<h1 class="text-xl font-bold">Secure Script Executor</h1> |
||||||
|
<button onclick="runScript()" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded transition duration-300 ease-in-out transform hover:scale-105"> |
||||||
|
Run Script |
||||||
|
</button> |
||||||
|
</nav> |
||||||
|
<div class="flex flex-1 overflow-hidden"> |
||||||
|
<div id="editor" class="w-1/2 border-r border-gray-300"></div> |
||||||
|
<div class="w-1/2 flex flex-col"> |
||||||
|
<div class="bg-gray-200 p-2 font-bold">Console Output</div> |
||||||
|
<div id="output" class="flex-1 overflow-auto bg-white p-2"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<footer class="bg-gray-200 p-2 text-center text-sm text-gray-600"> |
||||||
|
Secure Script Executor © 2023 |
||||||
|
</footer> |
||||||
|
|
||||||
|
<script> |
||||||
|
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs' } }) |
||||||
|
require(['vs/editor/editor.main'], function () { |
||||||
|
window.editor = monaco.editor.create(document.getElementById('editor'), { |
||||||
|
value: 'console.log("Hello World");', |
||||||
|
language: 'javascript', |
||||||
|
theme: 'vs-dark', |
||||||
|
automaticLayout: true, |
||||||
|
minimap: { enabled: false }, |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
let scriptWorker |
||||||
|
|
||||||
|
function createWorker() { |
||||||
|
const workerCode = ` |
||||||
|
'use strict'; |
||||||
|
// Security restrictions |
||||||
|
const restrictedGlobals = ['window', 'document', 'location', 'top', 'parent', 'frames', 'opener']; |
||||||
|
restrictedGlobals.forEach(name => { |
||||||
|
Object.defineProperty(self, name, { |
||||||
|
get: () => { |
||||||
|
throw new ReferenceError(name + ' is not defined'); |
||||||
|
}, |
||||||
|
configurable: false |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
// Restricted access to APIs |
||||||
|
self.XMLHttpRequest = undefined; |
||||||
|
self.fetch = undefined; |
||||||
|
self.WebSocket = undefined; |
||||||
|
self.localStorage = undefined; |
||||||
|
self.sessionStorage = undefined; |
||||||
|
|
||||||
|
self.console = { |
||||||
|
log: (...args) => self.postMessage({type: 'log', message: args.join(' ')}), |
||||||
|
error: (...args) => self.postMessage({type: 'error', message: args.join(' ')}), |
||||||
|
warn: (...args) => self.postMessage({type: 'warn', message: args.join(' ')}) |
||||||
|
}; |
||||||
|
|
||||||
|
// Importing NocoDB SDK in the worker |
||||||
|
import { Api } from 'https://cdn.jsdelivr.net/npm/nocodb-sdk@0.255.2/+esm'; |
||||||
|
|
||||||
|
// Initialize NocoDB SDK in the worker |
||||||
|
const api = new Api({ |
||||||
|
baseURL: 'http://localhost:8080', |
||||||
|
axiosConfig: { |
||||||
|
headers: { |
||||||
|
'xc-auth': 'undefined', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
self.onmessage = function(event) { |
||||||
|
const script = event.data.script; |
||||||
|
try { |
||||||
|
eval(script); |
||||||
|
} catch (error) { |
||||||
|
self.console.error(error.toString()); |
||||||
|
} |
||||||
|
self.postMessage({type: 'done'}); |
||||||
|
}; |
||||||
|
` |
||||||
|
|
||||||
|
const blob = new Blob([workerCode], { type: 'application/javascript' }) |
||||||
|
return new Worker(URL.createObjectURL(blob), { type: 'module' }) |
||||||
|
} |
||||||
|
|
||||||
|
function runScript() { |
||||||
|
const script = window.editor.getValue() |
||||||
|
const output = document.getElementById('output') |
||||||
|
output.innerHTML = '' |
||||||
|
|
||||||
|
if (scriptWorker) { |
||||||
|
scriptWorker.terminate() |
||||||
|
} |
||||||
|
|
||||||
|
scriptWorker = createWorker() |
||||||
|
|
||||||
|
scriptWorker.onmessage = function (event) { |
||||||
|
const { type, message } = event.data |
||||||
|
if (type === 'done') { |
||||||
|
scriptWorker.terminate() |
||||||
|
scriptWorker = null |
||||||
|
} else { |
||||||
|
const line = document.createElement('div') |
||||||
|
line.textContent = message |
||||||
|
line.className = `console-line ${type}` |
||||||
|
output.appendChild(line) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
scriptWorker.onerror = function (error) { |
||||||
|
const line = document.createElement('div') |
||||||
|
line.textContent = `Worker error: ${error.message}` |
||||||
|
line.className = 'console-line error' |
||||||
|
output.appendChild(line) |
||||||
|
scriptWorker.terminate() |
||||||
|
scriptWorker = null |
||||||
|
} |
||||||
|
|
||||||
|
scriptWorker.postMessage({ script }) |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,148 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { loadPyodide } from 'pyodide' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
modalVisible: boolean |
||||||
|
}>() |
||||||
|
|
||||||
|
const emits = defineEmits(['update:modalVisible']) |
||||||
|
|
||||||
|
const modalVisible = useVModel(props, 'modalVisible', emits) |
||||||
|
|
||||||
|
const { execScript } = useScripts() |
||||||
|
|
||||||
|
const script = ref('') |
||||||
|
|
||||||
|
const closeModel = () => { |
||||||
|
modalVisible.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const loading = ref(false) |
||||||
|
|
||||||
|
const response = ref('') |
||||||
|
|
||||||
|
const executeJSScript = async () => { |
||||||
|
loading.value = true |
||||||
|
try { |
||||||
|
const res = await execScript(script.value) |
||||||
|
response.value = JSON.stringify(res, null, 2) |
||||||
|
} catch (e) { |
||||||
|
response.value = e.message |
||||||
|
} finally { |
||||||
|
loading.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const executePythonScript = async () => { |
||||||
|
loading.value = true |
||||||
|
try { |
||||||
|
const pyodide = await loadPyodide() |
||||||
|
const res = await pyodide.runPythonAsync(script.value) |
||||||
|
response.value = JSON.stringify(res, null, 2) |
||||||
|
} catch (e) { |
||||||
|
response.value = e.message |
||||||
|
} finally { |
||||||
|
loading.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const supportedDocs = [ |
||||||
|
{ |
||||||
|
title: 'Scripting Guide', |
||||||
|
href: 'https://docs.nocodb.com/docs/scripting-guide', |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Scripting API', |
||||||
|
href: 'https://docs.nocodb.com/docs/scripting-api', |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Scripting Examples', |
||||||
|
href: 'https://docs.nocodb.com/docs/scripting-examples', |
||||||
|
}, |
||||||
|
] |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal v-model:visible="modalVisible" :show-separator="true" size="large" wrap-class-name="nc-modal-scripts-create-edit"> |
||||||
|
<template #header> |
||||||
|
<div class="flex w-full items-center p-4 justify-between"> |
||||||
|
<div class="flex items-center gap-3"> |
||||||
|
<GeneralIcon class="text-gray-900 text-2xl" icon="ncScript" /> |
||||||
|
<span class="text-gray-900 font-semibold text-xl"> Scripts Editor </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex justify-end items-center gap-3"> |
||||||
|
<NcButton :loading="loading" type="primary" size="small" data-testid="nc-test-script" @click="executeJSScript"> |
||||||
|
Test JS Script |
||||||
|
</NcButton> |
||||||
|
|
||||||
|
<NcButton :loading="loading" type="primary" size="small" data-testid="nc-test-script" @click="executePythonScript"> |
||||||
|
Test Python Script |
||||||
|
</NcButton> |
||||||
|
|
||||||
|
<NcButton type="text" size="small" data-testid="nc-close-scripts-modal" @click.stop="closeModel"> |
||||||
|
<GeneralIcon icon="close" /> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<div class="flex bg-white rounded-b-2xl h-[calc(100%_-_66px)]"> |
||||||
|
<div |
||||||
|
ref="containerElem" |
||||||
|
class="h-full flex-1 flex flex-col gap-8 overflow-y-auto scroll-smooth nc-scrollbar-thin px-12 py-6 mx-auto" |
||||||
|
> |
||||||
|
<iframe |
||||||
|
src="http://localhost:8080/api/v2/scripts" |
||||||
|
class="iframe-container" |
||||||
|
sandbox="allow-scripts allow-forms allow-same-origin allow-modals allow-popups allow-presentation" |
||||||
|
allow="geolocation *; microphone *; camera *; midi *; encrypted-media *;" |
||||||
|
referrerpolicy="no-referrer" |
||||||
|
loading="lazy" |
||||||
|
importance="high" |
||||||
|
height="100%" |
||||||
|
frameborder="0" |
||||||
|
scrolling="no" |
||||||
|
></iframe> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="h-full bg-gray-50 border-l-1 w-80 p-5 rounded-br-2xl border-gray-200"> |
||||||
|
<div class="w-full flex flex-col gap-3"> |
||||||
|
<h2 class="text-sm text-gray-700 font-semibold !my-0">{{ $t('labels.supportDocs') }}</h2> |
||||||
|
<div> |
||||||
|
<div v-for="(doc, idx) of supportedDocs" :key="idx" class="flex items-center gap-1"> |
||||||
|
<div class="h-7 w-7 flex items-center justify-center"> |
||||||
|
<GeneralIcon icon="bookOpen" class="flex-none w-4 h-4 text-gray-500" /> |
||||||
|
</div> |
||||||
|
<NuxtLink |
||||||
|
:href="doc.href" |
||||||
|
target="_blank" |
||||||
|
rel="noopener noreferrer" |
||||||
|
class="!text-gray-500 text-sm !no-underline !hover:underline" |
||||||
|
> |
||||||
|
{{ doc.title }} |
||||||
|
</NuxtLink> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.nc-modal-scripts-create-edit { |
||||||
|
z-index: 1050; |
||||||
|
a { |
||||||
|
@apply !no-underline !text-gray-700 !hover:text-primary; |
||||||
|
} |
||||||
|
.nc-modal { |
||||||
|
@apply !p-0; |
||||||
|
height: min(calc(100vh - 100px), 1024px); |
||||||
|
max-height: min(calc(100vh - 100px), 1024px) !important; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-modal-header { |
||||||
|
@apply !mb-0 !pb-0; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,17 @@ |
|||||||
|
export const useScripts = createSharedComposable(() => { |
||||||
|
const isScriptsEnabled = ref(false) |
||||||
|
|
||||||
|
const { $api } = useNuxtApp() |
||||||
|
|
||||||
|
const execScript = async (code: string) => { |
||||||
|
const data = $api.scripts.scriptExec({ |
||||||
|
body: { code }, |
||||||
|
}) |
||||||
|
|
||||||
|
console.log(data) |
||||||
|
|
||||||
|
return data |
||||||
|
} |
||||||
|
|
||||||
|
return { isScriptsEnabled, execScript } |
||||||
|
}) |
@ -0,0 +1,245 @@ |
|||||||
|
import { |
||||||
|
Body, |
||||||
|
Controller, |
||||||
|
Get, |
||||||
|
Post, |
||||||
|
Req, |
||||||
|
Res, |
||||||
|
UseGuards, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import { CodeInterpreter } from '@e2b/code-interpreter'; |
||||||
|
import Sandbox from 'v8-sandbox'; |
||||||
|
import { Response } from 'express'; |
||||||
|
import request from 'supertest'; |
||||||
|
import { GlobalGuard } from '~/guards/global/global.guard'; |
||||||
|
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; |
||||||
|
import { TenantContext } from '~/decorators/tenant-context.decorator'; |
||||||
|
import { NcContext, NcRequest } from '~/interface/config'; |
||||||
|
|
||||||
|
@Controller() |
||||||
|
@UseGuards(MetaApiLimiterGuard, GlobalGuard) |
||||||
|
export class ScriptsController { |
||||||
|
constructor() {} |
||||||
|
|
||||||
|
@Post(['/api/v2/scripts/exec']) |
||||||
|
async scriptExec( |
||||||
|
@TenantContext() context: NcContext, |
||||||
|
@Body() body, |
||||||
|
@Req() req: NcRequest, |
||||||
|
) { |
||||||
|
console.log(body); |
||||||
|
|
||||||
|
const sandbox = new Sandbox(); |
||||||
|
|
||||||
|
const { error, output, value } = await sandbox.execute({ |
||||||
|
code: body.code, |
||||||
|
timeout: 1000, |
||||||
|
}); |
||||||
|
|
||||||
|
console.log(value, output, error); |
||||||
|
|
||||||
|
// return output;
|
||||||
|
|
||||||
|
const sandboxE2b = await CodeInterpreter.create({ |
||||||
|
apiKey: 'e2b_28eedec39c0398d545f5cfa3e2b2332a13d3c4e2', |
||||||
|
}); |
||||||
|
const x = await sandboxE2b.notebook.execCell(body.code); |
||||||
|
|
||||||
|
console.log(x); |
||||||
|
|
||||||
|
return { x, output }; |
||||||
|
} |
||||||
|
|
||||||
|
@Get(['/api/v2/scripts']) |
||||||
|
async scriptHTML(@Res() res: Response, @Req() req: NcRequest) { |
||||||
|
res.setHeader('Content-Type', 'text/html'); |
||||||
|
res.setHeader('Cache-Control', 'no-store, max-age=0'); |
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff'); |
||||||
|
res.setHeader('Referrer-Policy', 'no-referrer'); |
||||||
|
res.setHeader( |
||||||
|
'Permissions-Policy', |
||||||
|
'geolocation=(), microphone=(), camera=(), midi=(), encrypted-media=()', |
||||||
|
); |
||||||
|
res.setHeader( |
||||||
|
'Strict-Transport-Security', |
||||||
|
'max-age=31536000; includeSubDomains; preload', |
||||||
|
); |
||||||
|
|
||||||
|
const cspDirectives = [ |
||||||
|
"default-src 'none'", |
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net", |
||||||
|
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com", |
||||||
|
`connect-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net http://localhost:8080`, |
||||||
|
"font-src 'self' https://cdnjs.cloudflare.com", |
||||||
|
"worker-src 'self' blob:", |
||||||
|
'child-src blob:', |
||||||
|
"base-uri 'none'", |
||||||
|
"form-action 'none'", |
||||||
|
]; |
||||||
|
|
||||||
|
// res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
|
||||||
|
|
||||||
|
res.send( |
||||||
|
generateHTML({ |
||||||
|
token: req.headers['Xc-Auth'] as string, |
||||||
|
url: 'http://localhost:8080', |
||||||
|
}), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const generateHTML = (config: { token: string; url: string }) => { |
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Secure Script Executor</title> |
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs/loader.js"></script> |
||||||
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> |
||||||
|
<style> |
||||||
|
body { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
height: 100vh; |
||||||
|
font-family: 'Arial', sans-serif; |
||||||
|
} |
||||||
|
#editor, #output { |
||||||
|
height: calc(100vh - 7rem); |
||||||
|
} |
||||||
|
.console-line { |
||||||
|
padding: 2px 5px; |
||||||
|
border-bottom: 1px solid #e2e8f0; |
||||||
|
} |
||||||
|
.log { color: #2d3748; } |
||||||
|
.error { color: #e53e3e; } |
||||||
|
.warn { color: #d69e2e; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body class="bg-gray-100"> |
||||||
|
<nav class="bg-blue-600 text-white p-4 flex justify-between items-center"> |
||||||
|
<h1 class="text-xl font-bold">Secure Script Executor</h1> |
||||||
|
<button onclick="runScript()" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded transition duration-300 ease-in-out transform hover:scale-105"> |
||||||
|
Run Script |
||||||
|
</button> |
||||||
|
</nav> |
||||||
|
<div class="flex flex-1 overflow-hidden"> |
||||||
|
<div id="editor" class="w-1/2 border-r border-gray-300"></div> |
||||||
|
<div class="w-1/2 flex flex-col"> |
||||||
|
<div class="bg-gray-200 p-2 font-bold">Console Output</div> |
||||||
|
<div id="output" class="flex-1 overflow-auto bg-white p-2"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<footer class="bg-gray-200 p-2 text-center text-sm text-gray-600"> |
||||||
|
Secure Script Executor © 2023 |
||||||
|
</footer> |
||||||
|
|
||||||
|
<script> |
||||||
|
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs' } }) |
||||||
|
require(['vs/editor/editor.main'], function () { |
||||||
|
window.editor = monaco.editor.create(document.getElementById('editor'), { |
||||||
|
value: 'console.log("Hello World");', |
||||||
|
language: 'javascript', |
||||||
|
theme: 'vs-dark', |
||||||
|
automaticLayout: true, |
||||||
|
minimap: { enabled: false }, |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
let scriptWorker; |
||||||
|
|
||||||
|
function createWorker() { |
||||||
|
const workerCode = \` |
||||||
|
'use strict'; |
||||||
|
// Security restrictions
|
||||||
|
const restrictedGlobals = ['window', 'document', 'location', 'top', 'parent', 'frames', 'opener']; |
||||||
|
restrictedGlobals.forEach(name => { |
||||||
|
Object.defineProperty(self, name, { |
||||||
|
get: () => { |
||||||
|
throw new ReferenceError(name + ' is not defined'); |
||||||
|
}, |
||||||
|
configurable: false |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
// Restricted access to APIs
|
||||||
|
self.XMLHttpRequest = undefined; |
||||||
|
self.fetch = undefined; |
||||||
|
self.WebSocket = undefined; |
||||||
|
self.localStorage = undefined; |
||||||
|
self.sessionStorage = undefined; |
||||||
|
|
||||||
|
self.console = { |
||||||
|
log: (...args) => self.postMessage({type: 'log', message: args.join(' ')}), |
||||||
|
error: (...args) => self.postMessage({type: 'error', message: args.join(' ')}), |
||||||
|
warn: (...args) => self.postMessage({type: 'warn', message: args.join(' ')}) |
||||||
|
}; |
||||||
|
|
||||||
|
// Importing NocoDB SDK in the worker
|
||||||
|
import { Api } from 'https://cdn.jsdelivr.net/npm/nocodb-sdk@0.255.2/+esm'; |
||||||
|
|
||||||
|
// Initialize NocoDB SDK in the worker
|
||||||
|
const api = new Api({ |
||||||
|
baseURL: '${config.url}', |
||||||
|
axiosConfig: { |
||||||
|
headers: { |
||||||
|
'xc-auth': '${config.token}', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
self.onmessage = function(event) { |
||||||
|
const script = event.data.script; |
||||||
|
try { |
||||||
|
eval(script); |
||||||
|
} catch (error) { |
||||||
|
self.console.error(error.toString()); |
||||||
|
} |
||||||
|
self.postMessage({type: 'done'}); |
||||||
|
}; |
||||||
|
\`;
|
||||||
|
|
||||||
|
const blob = new Blob([workerCode], { type: 'application/javascript' }); |
||||||
|
return new Worker(URL.createObjectURL(blob), { type: 'module' }); |
||||||
|
} |
||||||
|
|
||||||
|
function runScript() { |
||||||
|
const script = window.editor.getValue(); |
||||||
|
const output = document.getElementById('output'); |
||||||
|
output.innerHTML = ''; |
||||||
|
|
||||||
|
if (scriptWorker) { |
||||||
|
scriptWorker.terminate(); |
||||||
|
} |
||||||
|
|
||||||
|
scriptWorker = createWorker(); |
||||||
|
|
||||||
|
scriptWorker.onmessage = function (event) { |
||||||
|
const { type, message } = event.data; |
||||||
|
if (type === 'done') { |
||||||
|
scriptWorker.terminate(); |
||||||
|
scriptWorker = null; |
||||||
|
} else { |
||||||
|
const line = document.createElement('div'); |
||||||
|
line.textContent = message; |
||||||
|
line.className = 'console-line ' + type; |
||||||
|
output.appendChild(line); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
scriptWorker.onerror = function (error) { |
||||||
|
const line = document.createElement('div'); |
||||||
|
line.textContent = 'Worker error: ' + error.message; |
||||||
|
line.className = 'console-line error'; |
||||||
|
output.appendChild(line); |
||||||
|
scriptWorker.terminate(); |
||||||
|
scriptWorker = null; |
||||||
|
}; |
||||||
|
|
||||||
|
scriptWorker.postMessage({ script }); |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`;
|
||||||
|
}; |
Loading…
Reference in new issue