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 (
<>
{
>
+ }
+ onClick={handleTestCode}
+ >
+ Test Code
+
+ {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 } });
}