|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Run Python file in a new Batavia VM. |
| 4 | + */ |
| 5 | + |
| 6 | +const fs = require('fs') |
| 7 | +const path = require('path') |
| 8 | +const {spawnSync} = require('child_process') |
| 9 | +const batavia = require('./batavia/batavia.js') |
| 10 | + |
| 11 | +// The compile_module script is in the same directory as this script. |
| 12 | +const compileScriptPath = path.join(__dirname, 'compile_module.py') |
| 13 | + |
| 14 | +function displayHelpText() { |
| 15 | + console.log(` |
| 16 | +usage: [-h] file |
| 17 | +
|
| 18 | +Runs Python file in node JS using using the Batavia virtual machine in Node. |
| 19 | +
|
| 20 | +positional arguments: |
| 21 | + file Python file to run |
| 22 | + |
| 23 | +optional arguments: |
| 24 | + -h, --help show this help message and exit`.trim()) |
| 25 | +} |
| 26 | + |
| 27 | +function main() { |
| 28 | + if (process.argv.length === 3) { |
| 29 | + const argument = process.argv[2] |
| 30 | + |
| 31 | + if (argument === '-h' || argument === '--help') { |
| 32 | + displayHelpText() |
| 33 | + return |
| 34 | + } |
| 35 | + |
| 36 | + let resolveBase |
| 37 | + // The INIT_CWD environment variable is set in npm v5.4 and above. It |
| 38 | + // contains the directory in which the npm was run. If we have that |
| 39 | + // information we can resolve paths relative to it. |
| 40 | + // If the file path not relative and INIT_CWD is defined, we'll resolve |
| 41 | + // the provided path relative to it. |
| 42 | + if (process.env.INIT_CWD && !path.isAbsolute(argument[0])) { |
| 43 | + resolveBase = process.env.INIT_CWD |
| 44 | + } else { |
| 45 | + resolveBase = '' |
| 46 | + } |
| 47 | + |
| 48 | + const filePath = path.resolve(resolveBase, argument) |
| 49 | + const modulePath = path.basename(filePath, '.py') |
| 50 | + const basePath = path.dirname(filePath) |
| 51 | + |
| 52 | + fs.access( |
| 53 | + filePath, |
| 54 | + fs.constants.F_OK | fs.constants.R_OK, |
| 55 | + function(err) { |
| 56 | + if (err) { |
| 57 | + console.log( |
| 58 | + 'File "' + argument + '" does not exist ' + |
| 59 | + 'or cannot be accessed by current user.') |
| 60 | + } else { |
| 61 | + try { |
| 62 | + runInBatavia(basePath, modulePath) |
| 63 | + } catch (error) { |
| 64 | + if (error instanceof TypeError) { |
| 65 | + console.log('Invalid Python file.') |
| 66 | + } else throw error |
| 67 | + } |
| 68 | + } |
| 69 | + }) |
| 70 | + } else { |
| 71 | + displayHelpText() |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Runs the specified Python module |
| 77 | + * |
| 78 | + * @param basePath |
| 79 | + * @param module |
| 80 | + */ |
| 81 | +function runInBatavia(basePath, module) { |
| 82 | + const vm = new batavia.VirtualMachine({ |
| 83 | + loader: makeBataviaLoader(basePath), |
| 84 | + frame: null |
| 85 | + }) |
| 86 | + |
| 87 | + vm.run(module, []) |
| 88 | +} |
| 89 | + |
| 90 | +/** |
| 91 | + * Creates a loader function for the Batavia VM that looks for code around the |
| 92 | + * specified base path. |
| 93 | + * |
| 94 | + * @param basePath |
| 95 | + * @returns {bataviaLoader} |
| 96 | + */ |
| 97 | +function makeBataviaLoader(basePath) { |
| 98 | + /** |
| 99 | + * Compiles the specified Python module and returns its bytecode in the |
| 100 | + * format that Batavia expects. Returns null if module was not found so an |
| 101 | + * ImportError can be raised. |
| 102 | + * |
| 103 | + * @param modulePath |
| 104 | + * @returns {*} |
| 105 | + */ |
| 106 | + function bataviaLoader(modulePath) { |
| 107 | + const compileProcess = spawnSync( |
| 108 | + 'python3', |
| 109 | + [compileScriptPath, modulePath], |
| 110 | + {cwd: basePath} |
| 111 | + ) |
| 112 | + |
| 113 | + checkForErrors(compileProcess) |
| 114 | + |
| 115 | + const module = JSON.parse(compileProcess.stdout) |
| 116 | + |
| 117 | + // Module compiler will output null if the specified module was not |
| 118 | + // found. |
| 119 | + if (!module) { return null } |
| 120 | + |
| 121 | + return { |
| 122 | + '__python__': true, |
| 123 | + 'bytecode': module.bytecode.trim(), |
| 124 | + 'filename': module.filename.trim() |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + return bataviaLoader |
| 129 | +} |
| 130 | + |
| 131 | +/** |
| 132 | + * Checks the Python compile process result for errors. In case of error it |
| 133 | + * alerts the user quits the program. |
| 134 | + * |
| 135 | + * @param processResult |
| 136 | + */ |
| 137 | +function checkForErrors(processResult) { |
| 138 | + if (processResult.error) { |
| 139 | + console.log( |
| 140 | + 'There was an error running the Python compile process.\n' + |
| 141 | + 'Ensure that Python 3 is installed and available as "python3".') |
| 142 | + process.exit(1) |
| 143 | + } |
| 144 | + if (processResult.status !== 0) { |
| 145 | + console.log('There was an error during import.') |
| 146 | + console.log(processResult.stderr.toString()) |
| 147 | + process.exit(1) |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +if (require.main === module) { |
| 152 | + main() |
| 153 | +} |
0 commit comments