eez-open / studio

Cross-platform low-code GUI and automation
https://www.envox.eu/studio/studio-introduction/
GNU General Public License v3.0
451 stars 82 forks source link

[dashboard] Uncaught RuntimeError: memory access out of bounds #415

Open dong-king opened 2 months ago

dong-king commented 2 months ago

Describe the bug When I tested a certain action separately, it was normal. But when I put it into the project, it sometimes exposed a memory error:memory access out of bounds. I don't know if it is because my dashboard project has many business functions, which caused the memory to exceed the bounds.

Is the memory size defined in the memory.h file of eez-framework:

extern uint8_t g_memory[];
static uint8_t * const MEMORY_BEGIN = g_memory;
static const uint32_t MEMORY_SIZE = 64 * 1024 * 1024;

Is there any way to dynamically adjust the memory size?

Screenshots image

mvladic commented 2 months ago

I don't think this is memory size issue. If you can post the eez-project file with which I can reproduce this problem it would be of great help.

dong-king commented 2 months ago

Sometimes I want to execute a slightly complex js code directly in the action, so I customize an action based on your EvalJSExprActionComponent, as follows:

export class JsActionComponent extends ActionComponent {
    static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
        componentPaletteGroupName: "Dashboard Specific",
        label: () => "JS",
        componentPaletteLabel: "JS",
        properties: [
            {
                name: "expression",
                type: PropertyType.MultilineText,
                propertyGridGroup: specificGroup,
                monospaceFont: true,
                disableSpellcheck: true,
                flowProperty: "template-literal"
            },
            makeExpressionProperty(
                {
                    // The escape character corresponding to '{'
                    name: "leftEscape",
                    type: PropertyType.MultilineText,
                    propertyGridGroup: specificGroup
                },
                "string"
            ),
            makeExpressionProperty(
                {
                    // // The escape character corresponding to '}'
                    name: "rightEscape",
                    type: PropertyType.MultilineText,
                    propertyGridGroup: specificGroup
                },
                "string"
            )
        ],
        beforeLoadHook: (
            component: JsActionComponent,
            jsComponent: Partial<JsActionComponent>
        ) => {
            if (
                !jsComponent.customOutputs ||
                jsComponent.customOutputs.length == 0
            ) {
                jsComponent.customOutputs = [
                    {
                        name: "result",
                        type: "any"
                    }
                ] as any;
            }
        },
        check: (component: EvalJSExprActionComponent, messages: IMessage[]) => {
            const { valueExpressions } = component.expandExpressionForBuild();

            valueExpressions.forEach(valueExpression => {
                try {
                    checkExpression(component, valueExpression);
                } catch (err) {
                    messages.push(
                        new Message(
                            MessageType.ERROR,
                            `Invalid expression "${valueExpression}": ${err}`,
                            getChildOfObject(component, "expression")
                        )
                    );
                }
            });
        },
        icon: (
            <svg viewBox="0 0 1664 1792">
                <path d="M384 1536q0-53-37.5-90.5T256 1408t-90.5 37.5T128 1536t37.5 90.5T256 1664t90.5-37.5T384 1536zm384 0q0-53-37.5-90.5T640 1408t-90.5 37.5T512 1536t37.5 90.5T640 1664t90.5-37.5T768 1536zm-384-384q0-53-37.5-90.5T256 1024t-90.5 37.5T128 1152t37.5 90.5T256 1280t90.5-37.5T384 1152zm768 384q0-53-37.5-90.5T1024 1408t-90.5 37.5T896 1536t37.5 90.5 90.5 37.5 90.5-37.5 37.5-90.5zm-384-384q0-53-37.5-90.5T640 1024t-90.5 37.5T512 1152t37.5 90.5T640 1280t90.5-37.5T768 1152zM384 768q0-53-37.5-90.5T256 640t-90.5 37.5T128 768t37.5 90.5T256 896t90.5-37.5T384 768zm768 384q0-53-37.5-90.5T1024 1024t-90.5 37.5T896 1152t37.5 90.5 90.5 37.5 90.5-37.5 37.5-90.5zM768 768q0-53-37.5-90.5T640 640t-90.5 37.5T512 768t37.5 90.5T640 896t90.5-37.5T768 768zm768 768v-384q0-52-38-90t-90-38-90 38-38 90v384q0 52 38 90t90 38 90-38 38-90zm-384-768q0-53-37.5-90.5T1024 640t-90.5 37.5T896 768t37.5 90.5T1024 896t90.5-37.5T1152 768zm384-320V192q0-26-19-45t-45-19H192q-26 0-45 19t-19 45v256q0 26 19 45t45 19h1280q26 0 45-19t19-45zm0 320q0-53-37.5-90.5T1408 640t-90.5 37.5T1280 768t37.5 90.5T1408 896t90.5-37.5T1536 768zm128-640v1536q0 52-38 90t-90 38H128q-52 0-90-38t-38-90V128q0-52 38-90t90-38h1408q52 0 90 38t38 90z" />
            </svg>
        ),
        componentHeaderColor: "#A6BBCF",
        defaultValue: {
            leftEscape: `"#l#"`,
            rightEscape: `"#r#"`,
            customOutputs: [
                {
                    name: "result",
                    type: "any"
                }
            ]
        },

        execute: async (context: IDashboardComponentContext) => {
            // const expression = context.evalProperty<string>("expression");
            // console.log(expression);
            let leftEscape = context.evalProperty("leftEscape");
            if (
                leftEscape == undefined ||
                typeof leftEscape != "string" ||
                leftEscape.trim() == ""
            ) {
                context.throwError(`Invalid leftEscape property`);
                return;
            }
            let rightEscape = context.evalProperty("rightEscape");
            if (
                rightEscape == undefined ||
                typeof rightEscape != "string" ||
                rightEscape.trim() == ""
            ) {
                context.throwError(`Invalid rightEscape property`);
                return;
            }
            let expression = context.getStringParam(0);
            const expressionValues = context.getExpressionListParam(4);
            const values: any = {};
            for (let i = 0; i < expressionValues.length; i++) {
                const name = `_val${i}`;
                values[name] = expressionValues[i];
            }

            let regex = new RegExp(`${leftEscape}`, "g");
            expression = expression.replace(regex, `{`);

            regex = new RegExp(`${rightEscape}`, "g");
            expression = expression.replace(regex, `}`);

            context = context.startAsyncExecution();

            let param = {
                context,
                values
            };
            (async function (
                code: string,
                globalModules: { [moduleName: string]: any}) {
                try {
                    const moduleNames = Object.keys(globalModules);
                    const args = moduleNames.join(", ");
                    const factoryFnCode = `return async (${args}) => {
                        ${code}
                    }`;
                    const factoryFn = new Function(factoryFnCode);
                    console.log(factoryFnCode);
                    const fn = factoryFn();
                    await fn(
                        ...moduleNames.map(moduleName => globalModules[moduleName])
                    );
                } catch (err) {
                    context.throwError(err.toString());
                } finally {
                    context.endAsyncExecution();
                }
            })(expression || "", param);
        }
    });

    expression: string;

    constructor() {
        super();

        makeObservable(this, {
            expression: observable
        });
    }

    static parse(expression: string) {
        const inputs = new Set<string>();
        if (expression) {
            EvalJSExprActionComponent.PARAMS_REGEXP.lastIndex = 0;
            let str = expression;
            while (true) {
                let matches = str.match(
                    EvalJSExprActionComponent.PARAMS_REGEXP
                );
                if (!matches) {
                    break;
                }
                const input = matches[1].trim();
                inputs.add(input);
                str = str.substring(matches.index! + matches[1].length);
            }
        }

        return Array.from(inputs.keys());
    }

    getInputs() {
        return [
            {
                name: "@seqin",
                type: "any" as ValueType,
                isSequenceInput: true,
                isOptionalInput: true
            },
            ...super.getInputs()
        ];
    }

    getOutputs(): ComponentOutput[] {
        return [
            {
                name: "@seqout",
                type: "null",
                isSequenceOutput: true,
                isOptionalOutput: true
            },
            ...super.getOutputs()
        ];
    }

    getBody(flowContext: IFlowContext): React.ReactNode {
        return (
            <div className="body">
                <pre>{this.expression}</pre>
            </div>
        );
    }

    expandExpressionForBuild() {
        let expression = this.expression;
        let valueExpressions: any[] = [];

        JsActionComponent.parse(expression).forEach(
            (valueExpression: any, i: number) => {
                const name = `_val${i}`;
                valueExpressions.push(valueExpression);
                let regex = new RegExp(`{${valueExpression}}`, "g");
                expression = expression.replace(regex, `values.${name}`);
            }
        );

        return { expression, valueExpressions };
    }

    buildFlowComponentSpecific(assets: Assets, dataBuffer: DataBuffer) {
        const { expression, valueExpressions } =
            this.expandExpressionForBuild();

        dataBuffer.writeObjectOffset(() => dataBuffer.writeString(expression));

        dataBuffer.writeArray(valueExpressions, valueExpression => {
            try {
                // as property
                buildExpression(assets, dataBuffer, this, valueExpression);
            } catch (err) {
                assets.projectStore.outputSectionsStore.write(
                    Section.OUTPUT,
                    MessageType.ERROR,
                    err,
                    getChildOfObject(this, "expression")
                );

                dataBuffer.writeUint16NonAligned(makeEndInstruction());
            }
        });
    }
}

registerClass("JsActionComponent", JsActionComponent);

Because { and } cannot be used, I provide leftEscape (default is #l#) and rightEscape (default is #r#) to escape { and }.

The following screenshot is a simple usage: image This custom action works fine in most cases. But sometimes it prompts: Uncaught RuntimeError: memory access out of bounds and cannot read properties of null (reading 'context'). image

I cant find the reason.

BTW If you think this action is useful, you can merge it into eez-studio.

mvladic commented 2 months ago

BTW If you think this action is useful, you can merge it into eez-studio.

Yes, this is useful. I was aware of this issue with { and }, but I wasn't sure which way is the best to resolve it. I think maybe better option is to set left escape and right escape for the EEZ Flow expressions not for the JavaScript's { and }.

mvladic commented 2 months ago

BTW There is JSON.parse action, so you don't need to use EvalJS for that.

mvladic commented 2 months ago

I tried your Action with the same JavaScript code and I didn't see the problem so far. Can you tell me here:

image

What type you selected for the result output and in input?

mvladic commented 2 months ago

This particular error I had before:

image

and it is fixed now in the latest version of Studio. You are probably on some older version.

dong-king commented 2 months ago

What type you selected for the result output and in input?

result and in correspond to my custom structure. The previous screenshot is just to illustrate how to use this action, not the action that will cause an error.

In fact, an action that cause errors will not fail every time. I have a dashboard project with a lot of business logic. When I put the actions that cause errors in this project into a new dashboard project, they will not cause errors.

Maybe there are other reasons in this complex dashboard project that cause errors.

Basically, the errors occur when const factoryFn = new Function(factoryFnCode); or await fn( ...moduleNames.map(moduleName => globalModules[moduleName]) );is executed.

mvladic commented 2 months ago

Can you log the value of inputSelectInfoStr, I want to know the value when error occurs. Also, how structure WAVE_TABLESOURCE... is defined?

dong-king commented 2 months ago

This particular error I had before:

image

and it is fixed now in the latest version of Studio. You are probably on some older version. My colleague synchronized to version 0.13.0.

mvladic commented 2 months ago

Never mind, this error in lz4.js isn't important.