Kill previous worker if another eval/execute is asked to the donkey (#2218)

Kill previous worker if another eval/execute is asked to the donkey
This commit is contained in:
Andrea Giammarchi
2024-10-11 16:12:56 +02:00
committed by GitHub
parent 722abda895
commit b1c33b7f79
5 changed files with 129 additions and 63 deletions

29
core/package-lock.json generated
View File

@@ -1,16 +1,17 @@
{
"name": "@pyscript/core",
"version": "0.6.4",
"version": "0.6.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pyscript/core",
"version": "0.6.4",
"version": "0.6.5",
"license": "APACHE-2.0",
"dependencies": {
"@ungap/with-resolvers": "^0.1.0",
"@webreflection/idb-map": "^0.3.2",
"add-promise-listener": "^0.1.1",
"basic-devtools": "^0.1.6",
"polyscript": "^0.16.2",
"sabayon": "^0.5.2",
@@ -1018,6 +1019,12 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/add-promise-listener": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/add-promise-listener/-/add-promise-listener-0.1.1.tgz",
"integrity": "sha512-b3DQJ4VBQ1e4bjVPd0mqHkgFt4MYD8jYTEcfN9Qx+bGYs+WRLxPDnX7fRztGRm1k4CZzeXWvvYif6L1q4TToqg==",
"license": "MIT"
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1197,9 +1204,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001667",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
"integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
"version": "1.0.30001668",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
"dev": true,
"funding": [
{
@@ -1626,9 +1633,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.33",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
"integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
"version": "1.5.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
"integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==",
"dev": true,
"license": "ISC"
},
@@ -2342,9 +2349,9 @@
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"version": "0.30.12",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
"integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/core",
"version": "0.6.4",
"version": "0.6.5",
"type": "module",
"description": "PyScript",
"module": "./index.js",
@@ -60,6 +60,7 @@
"dependencies": {
"@ungap/with-resolvers": "^0.1.0",
"@webreflection/idb-map": "^0.3.2",
"add-promise-listener": "^0.1.1",
"basic-devtools": "^0.1.6",
"polyscript": "^0.16.2",
"sabayon": "^0.5.2",

View File

@@ -1,64 +1,108 @@
import addPromiseListener from "add-promise-listener";
import { assign, dedent } from "polyscript/exports";
const { stringify } = JSON;
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
export default (options = {}) => {
const type = options.type || "py";
const args = options.persistent
? ["globals()", "__locals__"]
: ["{}", "{}"];
const donkey = ({ type = "py", persistent, terminal, config }) => {
const args = persistent ? ["globals()", "__locals__"] : ["{}", "{}"];
const src = URL.createObjectURL(
new Blob([
dedent(`
from pyscript import sync, config
__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"
__locals__ = {}
if config["type"] == "py":
import sys
def __error__(_):
info = sys.exc_info()
return __message__(info[0], info[1])
else:
__error__ = lambda e: __message__(e.__class__, e.value)
def execute(code):
try: return ${invoke("exec", args)};
except Exception as e: print(__error__(e));
def evaluate(code):
try: return ${invoke("eval", args)};
except Exception as e: print(__error__(e));
sync.execute = execute
sync.evaluate = evaluate
`),
[
// this array is to better minify this code once in production
"from pyscript import sync, config",
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
"__locals__ = {}",
'if config["type"] == "py":',
" import sys",
" def __error__(_):",
" info = sys.exc_info()",
" return __message__(info[0], info[1])",
"else:",
" __error__ = lambda e: __message__(e.__class__, e.value)",
"def execute(code):",
` try: return ${invoke("exec", args)};`,
" except Exception as e: print(__error__(e));",
"def evaluate(code):",
` try: return ${invoke("eval", args)};`,
" except Exception as e: print(__error__(e));",
"sync.execute = execute",
"sync.evaluate = evaluate",
].join("\n"),
]),
);
// create the script that exposes the code to execute or evaluate
const script = assign(document.createElement("script"), { type, src });
script.toggleAttribute("worker", true);
script.toggleAttribute("terminal", true);
if (options.terminal) script.setAttribute("target", options.terminal);
if (options.config)
script.setAttribute("config", JSON.stringify(options.config));
if (terminal) script.setAttribute("target", terminal);
if (config) {
script.setAttribute(
"config",
typeof config === "string" ? config : stringify(config),
);
}
return new Promise((resolve) => {
script.addEventListener(`${type}:done`, (event) => {
event.stopPropagation();
URL.revokeObjectURL(src);
const { xworker, process, terminal } = script;
const { execute, evaluate } = xworker.sync;
script.remove();
resolve({
process,
execute: (code) => execute(dedent(code)),
evaluate: (code) => evaluate(dedent(code)),
clear: () => terminal.clear(),
reset: () => terminal.reset(),
kill: () => {
xworker.terminate();
terminal.dispose();
},
});
});
document.body.append(script);
return addPromiseListener(
document.body.appendChild(script),
`${type}:done`,
{ stopPropagation: true },
).then(() => {
URL.revokeObjectURL(src);
return script;
});
};
const utils = async (options) => {
const script = await donkey(options);
const { xworker, process, terminal } = script;
const { execute, evaluate } = xworker.sync;
script.remove();
return {
xworker,
process,
terminal,
execute,
evaluate,
};
};
export default async (options = {}) => {
let farmer = await utils(options);
let working = false;
const asyncTask = (method) => async (code) => {
// race condition ... a new task has been
// assigned while the previous one didn't finish
if (working) {
kill();
farmer = await utils(options);
}
working = true;
try {
return await farmer[method](dedent(code));
} catch (e) {
console.error(e);
} finally {
working = false;
}
};
const kill = () => {
if (farmer) {
farmer.xworker.terminate();
farmer.terminal.dispose();
farmer = null;
working = false;
}
};
return {
process: asyncTask("process"),
execute: asyncTask("execute"),
evaluate: asyncTask("evaluate"),
clear: () => farmer?.terminal.clear(),
reset: () => farmer?.terminal.reset(),
kill,
};
};

View File

@@ -1,4 +1,11 @@
export function donkey(options: any): Promise<any>;
export function donkey(options: any): Promise<{
process: (code: any) => Promise<any>;
execute: (code: any) => Promise<any>;
evaluate: (code: any) => Promise<any>;
clear: () => any;
reset: () => any;
kill: () => void;
}>;
export function offline_interpreter(config: any): string;
import { stdlib } from "./stdlib.js";
import { optional } from "./stdlib.js";

View File

@@ -1,2 +1,9 @@
declare function _default(options?: {}): Promise<any>;
declare function _default(options?: {}): Promise<{
process: (code: any) => Promise<any>;
execute: (code: any) => Promise<any>;
evaluate: (code: any) => Promise<any>;
clear: () => any;
reset: () => any;
kill: () => void;
}>;
export default _default;