feat: add custom monitor test code

This commit is contained in:
moonrailgun 2024-01-01 17:27:29 +08:00
parent ae6cea568b
commit 3e30535187
4 changed files with 122 additions and 11 deletions

View File

@ -1,9 +1,61 @@
import { Button, Form } from 'antd'; import { Button, Form, Modal } from 'antd';
import React from 'react'; import React from 'react';
import { MonitorProvider } from './types'; import { MonitorProvider } from './types';
import { CodeEditor } from '../../CodeEditor'; 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(() => { 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: (
<div>
{logger.map(([type, time, ...args]) => (
<div className="mb-1">
{type === 'warn' ? (
<ColorTag label={type} colors={['orange']} />
) : type === 'error' ? (
<ColorTag label={type} colors={['red']} />
) : (
<ColorTag label={type} colors={['geekblue']} />
)}
<span className="mr-1 text-neutral-500">
{dayjs(time).format('HH:mm:ss')}
</span>
{args.join(' ')}
</div>
))}
<div>Usage: {usage}ms</div>
<div>
Result: <span className="font-semibold">{result}</span>
</div>
</div>
),
});
});
return ( return (
<> <>
<Form.Item <Form.Item
@ -13,6 +65,14 @@ export const MonitorCustom: React.FC = React.memo(() => {
> >
<CodeEditor height={320} /> <CodeEditor height={320} />
</Form.Item> </Form.Item>
<Button
loading={testScriptMutation.isLoading}
icon={<PlayCircleOutlined />}
onClick={handleTestCode}
>
Test Code
</Button>
{contextHolder}
</> </>
); );
}); });

View File

@ -13,17 +13,18 @@ export const custom: MonitorProvider<{
const { code } = monitor.payload; 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 -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 isolate = new ivm.Isolate({ memoryLimit: env.sandboxMemoryLimit });
const code = `${environmentScript}\n\n;(async () => {${_code}})()`; const code = `${environmentScript}\n\n;(async () => {${_code}})()`;
@ -33,7 +34,21 @@ async function runCodeInVM(_code: string) {
isolate.compileScript(code), 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, { const res = await script.run(context, {
promise: true, promise: true,
@ -42,5 +57,5 @@ async function runCodeInVM(_code: string) {
context.release(); context.release();
script.release(); script.release();
return res; return { logger, result: res, usage: Date.now() - start };
} }

View File

@ -22,6 +22,7 @@ import {
import { OPENAPI_TAG } from '../../utils/const'; import { OPENAPI_TAG } from '../../utils/const';
import { OpenApiMeta } from 'trpc-openapi'; import { OpenApiMeta } from 'trpc-openapi';
import { MonitorStatusPageModelSchema } from '../../../../prisma/zod'; import { MonitorStatusPageModelSchema } from '../../../../prisma/zod';
import { runCodeInVM } from '../../model/monitor/provider/custom';
export const monitorRouter = router({ export const monitorRouter = router({
all: workspaceProcedure all: workspaceProcedure
@ -172,6 +173,22 @@ export const monitorRouter = router({
return monitorManager.delete(workspaceId, monitorId); 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 data: workspaceProcedure
.meta( .meta(
buildMonitorOpenapi({ buildMonitorOpenapi({

View File

@ -125,11 +125,30 @@ function makeTransferable(data: any) {
: new ivm.ExternalCopy(copyObject(data)).copyInto(); : 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; const jail = context.global;
jail.setSync('global', jail.derefInto()); jail.setSync('global', jail.derefInto());
jail.setSync('ivm', ivm); jail.setSync('_ivm', ivm);
jail.setSync('console', makeTransferable(console)); jail.setSync(
'console',
makeTransferable(globals.console ?? defaultSandboxGlobals)
);
jail.setSync( jail.setSync(
'_request', '_request',
new ivm.Reference(async (config: AxiosRequestConfig) => { new ivm.Reference(async (config: AxiosRequestConfig) => {
@ -153,7 +172,7 @@ const reproxy = (reference) => {
const data = reference.get(p); 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 } }); return (...args) => data.apply(undefined, args, { arguments: { copy: true }, result: { promise: true } });
} }