dcp-client
Version:
Core libraries for accessing DCP network
198 lines (168 loc) • 6.62 kB
JavaScript
/**
* @file pyodide-worktime.js
* This file sets up the hooks for the Pyodide worktime. It registers the processSlice
* initSandbox hooks, which run slices and setup the sandbox respectively.
*
* @author Wes Garland <wes@distributive.network>
* @author Severn Lortie <severn@distributive.network>
* @author Will Pringle <will@distributive.network>
* @author Hamada Gasmallah <hamada@distributive.network>
*
* @date February, 2024
* @copyright Copyright (c) 2018-2024, Distributive, Ltd. All Rights Reserved.
*/
'use-strict';
/* global self, bravojs, addEventListener, postMessage */
// @ts-nocheck
self.wrapScriptLoading({ scriptName: 'pyodide-worktime'}, function pyodideWorktime$$fn(protectedStorage, ring1PostMessage, wrapPostMessage)
{
let wrappedPythonSliceHandler;
/**
* Registers the worktime callbacks that allow the worktime controller (worktimes.js) to setup our environment and run the worktime
*/
function registerWorktime()
{
protectedStorage.worktimes.registerWorktime({
name: 'pyodide',
version: '0.28.0',
processSlice,
initSandbox,
});
}
/**
* Function which generates a "map-basic"-like workFunction
* out of a Pyodide Worktime job (Python code, files, env variables).
*
* It takes any "images" passed in the workFunction "arguments" and
* writes them to the in memory filesystem provided by Emscripten.
* It adds any environment variables specified in the workFunction
* "arguments" to the pseudo-"process" for use.
* It globally imports a dcp module with function "set_slice_handler"
* which takes a python function as input. The python function passed
* to that slice handler is invoked by the function which this
* function creates.
*
* @param {Object} job The job being assigned to the sandbox
*/
async function initSandbox(job)
{
var pythonSliceHandler;
const pyodide = await pyodideInit();
const sys = pyodide.pyimport('sys');
const findImports = pyodide.runPython('import pyodide; pyodide.code.find_imports');
const findPythonModuleLoader = pyodide.runPython('import importlib; importlib.util.find_spec');
const parsedArguments = parsePyodideArguments(job.arguments);
// write images to file and set environment variables
const prepPyodide = pyodide.runPython(`
import tarfile, io
import os, sys
def prepPyodide(args):
for image in args['images']:
image = bytes(image)
imageFile = io.BytesIO(image)
tar = tarfile.open(mode='r', fileobj=imageFile)
# Don't overwrite directories which corrupts Pyodide's in memory filesystem
def safe_extract(tar, path="/"):
for member in tar.getmembers():
if member.isdir():
dir_path = os.path.join(path, member.name)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
else:
tar.extract(member, path)
safe_extract(tar)
for item, value in args['environmentVariables'].items():
os.environ[item] = value
sys.argv.extend(args['sysArgv'])
return
prepPyodide`);
prepPyodide(pyodide.toPy(parsedArguments));
// register the dcp Python module
if (!sys.modules.get('dcp'))
{
const create_proxy = pyodide.runPython('import pyodide;pyodide.ffi.create_proxy');
pyodide.registerJsModule('dcp', {
set_slice_handler: function pyodide$$dcp$$setSliceHandler(func) {
pythonSliceHandler = create_proxy(func);
},
progress,
});
}
pyodide.runPython( 'import dcp' );
// attempt to import packages from the package manager (if they're not already loaded)
const workFunctionPythonImports = findImports(job.workFunction).toJs();
const packageManagerImports = workFunctionPythonImports.filter(x=>!findPythonModuleLoader(x));
if (packageManagerImports.length > 0)
{
await fetchAndInitPyodidePackages(packageManagerImports);
await pyodide.loadPackage(packageManagerImports);
}
wrappedPythonSliceHandler = workFunctionWrapper;
/**
* Evaluates the Python WorkFunction string and then executes the slice
* handler Python function. If no call to `dcp.set_slice_handler` is passed
* or a non function is passed to it.
* This function specifically only takes one parameter since Pyodide Slice
* Handlers only accept one parameter.
*/
async function workFunctionWrapper(datum)
{
const pyodide = await pyodideInit(); // returns the same promise when called multiple times
// load and execute the Python Workfunction, this populates the pythonSliceHandler variable
await pyodide.runPython(job.workFunction);
// failure to specify a slice handler is considered an error
if (!pythonSliceHandler)
throw new Error('ENOSLICEHANDLER: Must specify the slice handler using `dcp.set_slice_handler(fn)`');
// setting the slice handler to a non function or lambda is not supported
else if (typeof pythonSliceHandler !== 'function')
throw new Error('ENOSLICEHANDLER: Slice Handler must be a function');
const sliceHandlerResult = await pythonSliceHandler(pyodide.toPy(datum));
// if it is a PyProxy, convert its value to JavaScript
if (sliceHandlerResult?.toJs)
return sliceHandlerResult.toJs();
return sliceHandlerResult;
}
/*
* Refer to the "The Pyodide Worktime"."Work Function (JS)"."Arguments"."Commands"
* part of the DCP Worktimes spec.
*/
function parsePyodideArguments(args)
{
var index = 1;
const numArgs = args[0];
const images = [];
const environmentVariables = {};
const sysArgv = args.slice(numArgs);
while (index < numArgs)
{
switch (args[index])
{
case 'gzImage':
const image = args[index+1];
images.push(image);
index+=2;
break;
case 'env':
const env = args[index+1].split(/=(.*)/s);
index+=2;
environmentVariables[env[0]] = env[1];
break;
default:
throw new Error(`Invalid argument ${args[index]}`);
}
}
return { sysArgv, images, environmentVariables };
}
}
/**
* The processSlice hook function which runs the work function for input slice data
* @param {*} data The slice data
* @returns {*} The slice result
*/
async function processSlice(data)
{
const result = await wrappedPythonSliceHandler(data);
return result;
}
registerWorktime();
}); /* end of fn */