diff --git a/src/client/components/monitor/provider/custom.tsx b/src/client/components/monitor/provider/custom.tsx index 71444c7..66dd1ca 100644 --- a/src/client/components/monitor/provider/custom.tsx +++ b/src/client/components/monitor/provider/custom.tsx @@ -1,9 +1,61 @@ -import { Button, Form } from 'antd'; +import { Button, Form, Modal } from 'antd'; import React from 'react'; import { MonitorProvider } from './types'; import { CodeEditor } from '../../CodeEditor'; +import { defaultErrorHandler, trpc } from '../../../api/trpc'; +import { PlayCircleOutlined } from '@ant-design/icons'; +import { useCurrentWorkspaceId } from '../../../store/user'; +import { useEvent } from '../../../hooks/useEvent'; +import dayjs from 'dayjs'; +import { ColorTag } from '../../ColorTag'; export const MonitorCustom: React.FC = React.memo(() => { + const workspaceId = useCurrentWorkspaceId(); + const testScriptMutation = trpc.monitor.testCustomScript.useMutation({ + onError: defaultErrorHandler, + }); + const form = Form.useFormInstance(); + const [modal, contextHolder] = Modal.useModal(); + + const handleTestCode = useEvent(async () => { + const { logger, result, usage } = await testScriptMutation.mutateAsync({ + workspaceId, + code: form.getFieldValue(['payload', 'code']), + }); + + modal.info({ + centered: true, + maskClosable: true, + title: 'Run Completed', + content: ( +
+ {logger.map(([type, time, ...args]) => ( +
+ {type === 'warn' ? ( + + ) : type === 'error' ? ( + + ) : ( + + )} + + + {dayjs(time).format('HH:mm:ss')} + + + {args.join(' ')} +
+ ))} + +
Usage: {usage}ms
+
+ Result: {result} +
+
+ ), + }); + }); + return ( <> { > + + {contextHolder} ); }); diff --git a/src/server/model/monitor/provider/custom.ts b/src/server/model/monitor/provider/custom.ts index 39c1c91..52bb9d2 100644 --- a/src/server/model/monitor/provider/custom.ts +++ b/src/server/model/monitor/provider/custom.ts @@ -13,17 +13,18 @@ export const custom: MonitorProvider<{ const { code } = monitor.payload; - const res = await runCodeInVM(code); + const { result } = await runCodeInVM(code); - if (typeof res !== 'number') { + if (typeof result !== 'number') { return -1; } - return res; + return result; }, }; -async function runCodeInVM(_code: string) { +export async function runCodeInVM(_code: string) { + const start = Date.now(); const isolate = new ivm.Isolate({ memoryLimit: env.sandboxMemoryLimit }); const code = `${environmentScript}\n\n;(async () => {${_code}})()`; @@ -33,7 +34,21 @@ async function runCodeInVM(_code: string) { isolate.compileScript(code), ]); - buildSandbox(context); + const logger: any[][] = []; + + buildSandbox(context, { + console: { + log: (...args: any[]) => { + logger.push(['log', Date.now(), ...args]); + }, + warn: (...args: any[]) => { + logger.push(['warn', Date.now(), ...args]); + }, + error: (...args: any[]) => { + logger.push(['error', Date.now(), ...args]); + }, + }, + }); const res = await script.run(context, { promise: true, @@ -42,5 +57,5 @@ async function runCodeInVM(_code: string) { context.release(); script.release(); - return res; + return { logger, result: res, usage: Date.now() - start }; } diff --git a/src/server/trpc/routers/monitor.ts b/src/server/trpc/routers/monitor.ts index 0494215..38ea9e8 100644 --- a/src/server/trpc/routers/monitor.ts +++ b/src/server/trpc/routers/monitor.ts @@ -22,6 +22,7 @@ import { import { OPENAPI_TAG } from '../../utils/const'; import { OpenApiMeta } from 'trpc-openapi'; import { MonitorStatusPageModelSchema } from '../../../../prisma/zod'; +import { runCodeInVM } from '../../model/monitor/provider/custom'; export const monitorRouter = router({ all: workspaceProcedure @@ -172,6 +173,22 @@ export const monitorRouter = router({ return monitorManager.delete(workspaceId, monitorId); }), + testCustomScript: workspaceOwnerProcedure + .input( + z.object({ + code: z.string(), + }) + ) + .output( + z.object({ + logger: z.array(z.array(z.any())), + result: z.number(), + usage: z.number(), + }) + ) + .mutation(async ({ input }) => { + return runCodeInVM(input.code); + }), data: workspaceProcedure .meta( buildMonitorOpenapi({ diff --git a/src/server/utils/sandbox.ts b/src/server/utils/sandbox.ts index 293df9b..bfa533e 100644 --- a/src/server/utils/sandbox.ts +++ b/src/server/utils/sandbox.ts @@ -125,11 +125,30 @@ function makeTransferable(data: any) { : new ivm.ExternalCopy(copyObject(data)).copyInto(); } -export function buildSandbox(context: Context) { +interface SandboxGlobals { + console?: { + log: (...args: any[]) => void; + warn: (...args: any[]) => void; + error: (...args: any[]) => void; + }; +} + +const defaultSandboxGlobals = { + console: { + log: () => {}, + warn: () => {}, + error: () => {}, + }, +}; + +export function buildSandbox(context: Context, globals: SandboxGlobals = {}) { const jail = context.global; jail.setSync('global', jail.derefInto()); - jail.setSync('ivm', ivm); - jail.setSync('console', makeTransferable(console)); + jail.setSync('_ivm', ivm); + jail.setSync( + 'console', + makeTransferable(globals.console ?? defaultSandboxGlobals) + ); jail.setSync( '_request', new ivm.Reference(async (config: AxiosRequestConfig) => { @@ -153,7 +172,7 @@ const reproxy = (reference) => { const data = reference.get(p); - if (typeof data === 'object' && data instanceof ivm.Reference && data.typeof === 'function') { + if (typeof data === 'object' && data instanceof _ivm.Reference && data.typeof === 'function') { return (...args) => data.apply(undefined, args, { arguments: { copy: true }, result: { promise: true } }); }