import React from "react"; import { tmpdir } from "os"; import { sep } from "path"; import { writeFileSync, unlinkSync } from "fs"; import type { PythonShell, Options } from "python-shell"; import type { IDashboardComponentContext, IWasmFlowRuntime } from "eez-studio-types"; import { onWasmFlowRuntimeTerminate } from "project-editor/flow/runtime/wasm-worker"; import { registerActionComponents } from "project-editor/flow/component"; import { PYTHON_ICON, RightArrow } from "project-editor/ui-components/icons"; import { settingsController } from "home/settings"; const componentHeaderColor = "#BBE4E0"; registerActionComponents("Python", [ { name: "PythonRun", icon: PYTHON_ICON as any, componentHeaderColor, inputs: [], outputs: [ { name: "handle", type: "integer", isSequenceOutput: false, isOptionalOutput: true }, { name: "message", type: "string", isSequenceOutput: false, isOptionalOutput: true } ], properties: [ { name: "scriptSourceOption", type: "enum", enumItems: [ { id: "inline-script", label: "Inline script" }, { id: "inline-script-as-expression", label: "Inline script as expression" }, { id: "script-file", label: "Script file" } ] }, { name: "scriptSourceInline", displayName: "Inline script", type: "inline-code", language: "Python", disabled: (...props: string[]) => { return props[0] != "inline-script"; } }, { name: "scriptSourceInlineFromExpression", displayName: "Inline script as expression", type: "expression", valueType: "string", disabled: (...props: string[]) => { return props[0] != "inline-script-as-expression"; } }, { name: "scriptSourceFile", displayName: "Script file", type: "expression", valueType: "string", disabled: (...props: string[]) => { return props[0] != "script-file"; } }, { name: "pythonPath", type: "expression", valueType: "string" } ], defaults: { scriptSourceOption: "inline-script", pythonPath: '""' }, bodyPropertyCallback: (...props: string[]) => { if (props[0] == "inline-script") { return (
                        {props[1]}
                    
); } else if (props[0] == "inline-script-as-expression") { return props[2]; } else { return props[3]; } }, execute: (context: IDashboardComponentContext) => { const scriptSourceOption = context.getStringParam(0); let scriptSource; let isInlineScript: boolean; if (scriptSourceOption == "inline-script") { scriptSource = context.getStringParam(4); isInlineScript = true; } else if (scriptSourceOption == "inline-script-as-expression") { scriptSource = context.evalProperty( "scriptSourceInlineFromExpression" ); isInlineScript = true; } else { scriptSource = context.evalProperty("scriptSourceFile"); isInlineScript = false; } if (scriptSource == undefined) { context.throwError(`Invalid script source`); return; } const handle = getNextPythonShellHandle(); let scriptFilePath: string; if (isInlineScript) { scriptFilePath = tmpdir() + sep + `eez-runtime-python-script-${handle}.py`; writeFileSync(scriptFilePath, scriptSource); } else { scriptFilePath = scriptSource; } let pythonPath = context.evalProperty("pythonPath"); if (!pythonPath) { if (settingsController.pythonUseCustomPath) { pythonPath = settingsController.pythonCustomPath; } } const options: Options = { pythonPath }; const { PythonShell } = require("python-shell") as typeof import("python-shell"); const pythonShell = new PythonShell(scriptFilePath, options); addPythonShell(context.WasmFlowRuntime, pythonShell); pythonShell.on("message", message => { const { wasmFlowRuntimeTerminated } = getPythonShell(handle); if (!wasmFlowRuntimeTerminated) { context.propagateValue("message", message); } }); let wasError = false; function cleanup() { removePythonShell(handle); if (isInlineScript) { unlinkSync(scriptFilePath); } } pythonShell.on("close", () => { if (!wasError) { const { wasmFlowRuntimeTerminated } = getPythonShell(handle); if (!wasmFlowRuntimeTerminated) { context.propagateValueThroughSeqout(); } cleanup(); } }); pythonShell.on("pythonError", err => { wasError = true; const { wasmFlowRuntimeTerminated } = getPythonShell(handle); if (!wasmFlowRuntimeTerminated) { context.throwError(err.toString()); } cleanup(); }); pythonShell.on("error", err => { wasError = true; const { wasmFlowRuntimeTerminated } = getPythonShell(handle); if (!wasmFlowRuntimeTerminated) { context.throwError(err.toString()); } cleanup(); }); context.propagateValue("handle", handle); } }, { name: "PythonSendMessage", icon: PYTHON_ICON as any, componentHeaderColor, inputs: [], outputs: [], properties: [ { name: "handle", type: "expression", valueType: "integer" }, { name: "message", type: "expression", valueType: "string" } ], bodyPropertyCallback(...props) { return (
                    
                        {props[1]}
                        
                        {props[0]}
                    
                
); }, defaults: { handle: "handle", customInputs: [ { name: "handle", type: "integer" } ] }, execute: (context: IDashboardComponentContext) => { const handle = context.evalProperty("handle"); if (!handle) { context.throwError(`Handle not defined`); return; } const { pythonShell } = getPythonShell(handle); if (!pythonShell) { context.throwError(`Invalid handle ` + handle); return; } const message = context.evalProperty("message"); if (!message) { context.throwError(`Message not defined`); return; } pythonShell.send(message.toString()); context.propagateValueThroughSeqout(); } }, { name: "PythonEnd", icon: PYTHON_ICON as any, componentHeaderColor, inputs: [], outputs: [], properties: [ { name: "handle", type: "expression", valueType: "integer" } ], defaults: { handle: "handle", customInputs: [ { name: "handle", type: "integer" } ] }, bodyPropertyName: "handle", execute: (context: IDashboardComponentContext) => { const handle = context.evalProperty("handle"); if (!handle) { context.throwError(`Handle not defined`); return; } const { pythonShell } = getPythonShell(handle); if (!pythonShell) { context.throwError(`Invalid handle ` + handle); return; } pythonShell.end(() => { context.propagateValueThroughSeqout(); }); } } ]); //////////////////////////////////////////////////////////////////////////////// const handleToPythonShell = new Map< number, { wasmFlowRuntime: IWasmFlowRuntime; wasmFlowRuntimeTerminated: boolean; pythonShell: PythonShell; } >(); let lastPythonShellHandle = 0; function getNextPythonShellHandle() { return lastPythonShellHandle + 1; } function addPythonShell( wasmFlowRuntime: IWasmFlowRuntime, pythonShell: PythonShell ) { lastPythonShellHandle = getNextPythonShellHandle(); handleToPythonShell.set(lastPythonShellHandle, { wasmFlowRuntime, wasmFlowRuntimeTerminated: false, pythonShell }); return lastPythonShellHandle; } function getPythonShell(handle: number) { return ( handleToPythonShell.get(handle) || { pythonShell: undefined, wasmFlowRuntime: undefined, wasmFlowRuntimeTerminated: true } ); } function removePythonShell(handle: number) { handleToPythonShell.delete(handle); } onWasmFlowRuntimeTerminate((wasmFlowRuntime: IWasmFlowRuntime) => { for (const item of handleToPythonShell) { if (item[1].wasmFlowRuntime == wasmFlowRuntime) { item[1].wasmFlowRuntimeTerminated = true; item[1].pythonShell.end(() => {}); } } });