node/lib/vm.js

419 lines
13 KiB
JavaScript

// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const {
ArrayPrototypeForEach,
ObjectFreeze,
PromiseReject,
ReflectApply,
Symbol,
} = primordials;
const {
ContextifyScript,
makeContext,
constants,
measureMemory: _measureMemory,
} = internalBinding('contextify');
const {
ERR_CONTEXT_NOT_INITIALIZED,
ERR_INVALID_ARG_TYPE,
} = require('internal/errors').codes;
const {
validateArray,
validateBoolean,
validateBuffer,
validateInt32,
validateOneOf,
validateObject,
validateString,
validateStringArray,
validateUint32,
kValidateObjectAllowArray,
kValidateObjectAllowNullable,
} = require('internal/validators');
const {
emitExperimentalWarning,
kEmptyObject,
kVmBreakFirstLineSymbol,
} = require('internal/util');
const {
getHostDefinedOptionId,
internalCompileFunction,
isContext: _isContext,
registerImportModuleDynamically,
} = require('internal/vm');
const {
vm_dynamic_import_main_context_default,
vm_context_no_contextify,
} = internalBinding('symbols');
const kParsingContext = Symbol('script parsing context');
/**
* Check if object is a context object created by vm.createContext().
* @throws {TypeError} If object is not an object in the first place, throws TypeError.
* @param {object} object Object to check.
* @returns {boolean}
*/
function isContext(object) {
validateObject(object, 'object', kValidateObjectAllowArray);
return _isContext(object);
}
class Script extends ContextifyScript {
constructor(code, options = kEmptyObject) {
code = `${code}`;
if (typeof options === 'string') {
options = { filename: options };
} else {
validateObject(options, 'options');
}
const {
filename = 'evalmachine.<anonymous>',
lineOffset = 0,
columnOffset = 0,
cachedData,
produceCachedData = false,
importModuleDynamically,
[kParsingContext]: parsingContext,
} = options;
validateString(filename, 'options.filename');
validateInt32(lineOffset, 'options.lineOffset');
validateInt32(columnOffset, 'options.columnOffset');
if (cachedData !== undefined) {
validateBuffer(cachedData, 'options.cachedData');
}
validateBoolean(produceCachedData, 'options.produceCachedData');
const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, filename);
// Calling `ReThrow()` on a native TryCatch does not generate a new
// abort-on-uncaught-exception check. A dummy try/catch in JS land
// protects against that.
try { // eslint-disable-line no-useless-catch
super(code,
filename,
lineOffset,
columnOffset,
cachedData,
produceCachedData,
parsingContext,
hostDefinedOptionId);
} catch (e) {
throw e; /* node-do-not-add-exception-line */
}
registerImportModuleDynamically(this, importModuleDynamically);
}
runInThisContext(options) {
const { breakOnSigint, args } = getRunInContextArgs(null, options);
if (breakOnSigint && process.listenerCount('SIGINT') > 0) {
return sigintHandlersWrap(super.runInContext, this, args);
}
return ReflectApply(super.runInContext, this, args);
}
runInContext(contextifiedObject, options) {
validateContext(contextifiedObject);
const { breakOnSigint, args } = getRunInContextArgs(
contextifiedObject,
options,
);
if (breakOnSigint && process.listenerCount('SIGINT') > 0) {
return sigintHandlersWrap(super.runInContext, this, args);
}
return ReflectApply(super.runInContext, this, args);
}
runInNewContext(contextObject, options) {
const context = createContext(contextObject, getContextOptions(options));
return this.runInContext(context, options);
}
}
function validateContext(contextifiedObject) {
if (!isContext(contextifiedObject)) {
throw new ERR_INVALID_ARG_TYPE('contextifiedObject', 'vm.Context',
contextifiedObject);
}
}
function getRunInContextArgs(contextifiedObject, options = kEmptyObject) {
validateObject(options, 'options');
let timeout = options.timeout;
if (timeout === undefined) {
timeout = -1;
} else {
validateUint32(timeout, 'options.timeout', true);
}
const {
displayErrors = true,
breakOnSigint = false,
[kVmBreakFirstLineSymbol]: breakFirstLine = false,
} = options;
validateBoolean(displayErrors, 'options.displayErrors');
validateBoolean(breakOnSigint, 'options.breakOnSigint');
return {
breakOnSigint,
args: [
contextifiedObject,
timeout,
displayErrors,
breakOnSigint,
breakFirstLine,
],
};
}
function getContextOptions(options) {
if (!options)
return {};
const contextOptions = {
name: options.contextName,
origin: options.contextOrigin,
codeGeneration: undefined,
microtaskMode: options.microtaskMode,
};
if (contextOptions.name !== undefined)
validateString(contextOptions.name, 'options.contextName');
if (contextOptions.origin !== undefined)
validateString(contextOptions.origin, 'options.contextOrigin');
if (options.contextCodeGeneration !== undefined) {
validateObject(options.contextCodeGeneration,
'options.contextCodeGeneration');
const { strings, wasm } = options.contextCodeGeneration;
if (strings !== undefined)
validateBoolean(strings, 'options.contextCodeGeneration.strings');
if (wasm !== undefined)
validateBoolean(wasm, 'options.contextCodeGeneration.wasm');
contextOptions.codeGeneration = { strings, wasm };
}
if (options.microtaskMode !== undefined)
validateString(options.microtaskMode, 'options.microtaskMode');
return contextOptions;
}
let defaultContextNameIndex = 1;
function createContext(contextObject = {}, options = kEmptyObject) {
if (contextObject !== vm_context_no_contextify && isContext(contextObject)) {
return contextObject;
}
validateObject(options, 'options');
const {
name = `VM Context ${defaultContextNameIndex++}`,
origin,
codeGeneration,
microtaskMode,
importModuleDynamically,
} = options;
validateString(name, 'options.name');
if (origin !== undefined)
validateString(origin, 'options.origin');
if (codeGeneration !== undefined)
validateObject(codeGeneration, 'options.codeGeneration');
let strings = true;
let wasm = true;
if (codeGeneration !== undefined) {
({ strings = true, wasm = true } = codeGeneration);
validateBoolean(strings, 'options.codeGeneration.strings');
validateBoolean(wasm, 'options.codeGeneration.wasm');
}
validateOneOf(microtaskMode,
'options.microtaskMode',
['afterEvaluate', undefined]);
const microtaskQueue = (microtaskMode === 'afterEvaluate');
const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, name);
const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
// Register the context scope callback after the context was initialized.
registerImportModuleDynamically(result, importModuleDynamically);
return result;
}
function createScript(code, options) {
return new Script(code, options);
}
// Remove all SIGINT listeners and re-attach them after the wrapped function
// has executed, so that caught SIGINT are handled by the listeners again.
function sigintHandlersWrap(fn, thisArg, argsArray) {
const sigintListeners = process.rawListeners('SIGINT');
process.removeAllListeners('SIGINT');
try {
return ReflectApply(fn, thisArg, argsArray);
} finally {
// Add using the public methods so that the `newListener` handler of
// process can re-attach the listeners.
ArrayPrototypeForEach(sigintListeners, (listener) => {
process.addListener('SIGINT', listener);
});
}
}
function runInContext(code, contextifiedObject, options) {
validateContext(contextifiedObject);
if (typeof options === 'string') {
options = {
filename: options,
[kParsingContext]: contextifiedObject,
};
} else {
options = { ...options, [kParsingContext]: contextifiedObject };
}
return createScript(code, options)
.runInContext(contextifiedObject, options);
}
function runInNewContext(code, contextObject, options) {
if (typeof options === 'string') {
options = { filename: options };
}
contextObject = createContext(contextObject, getContextOptions(options));
options = { ...options, [kParsingContext]: contextObject };
return createScript(code, options).runInNewContext(contextObject, options);
}
function runInThisContext(code, options) {
if (typeof options === 'string') {
options = { filename: options };
}
return createScript(code, options).runInThisContext(options);
}
function compileFunction(code, params, options = kEmptyObject) {
validateString(code, 'code');
if (params !== undefined) {
validateStringArray(params, 'params');
}
const {
filename = '',
columnOffset = 0,
lineOffset = 0,
cachedData = undefined,
produceCachedData = false,
parsingContext = undefined,
contextExtensions = [],
importModuleDynamically,
} = options;
validateString(filename, 'options.filename');
validateInt32(columnOffset, 'options.columnOffset');
validateInt32(lineOffset, 'options.lineOffset');
if (cachedData !== undefined)
validateBuffer(cachedData, 'options.cachedData');
validateBoolean(produceCachedData, 'options.produceCachedData');
if (parsingContext !== undefined) {
if (
typeof parsingContext !== 'object' ||
parsingContext === null ||
!isContext(parsingContext)
) {
throw new ERR_INVALID_ARG_TYPE(
'options.parsingContext',
'Context',
parsingContext,
);
}
}
validateArray(contextExtensions, 'options.contextExtensions');
ArrayPrototypeForEach(contextExtensions, (extension, i) => {
const name = `options.contextExtensions[${i}]`;
validateObject(extension, name, kValidateObjectAllowNullable);
});
const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, filename);
return internalCompileFunction(
code, filename, lineOffset, columnOffset,
cachedData, produceCachedData, parsingContext, contextExtensions,
params, hostDefinedOptionId, importModuleDynamically,
).function;
}
const measureMemoryModes = {
summary: constants.measureMemory.mode.SUMMARY,
detailed: constants.measureMemory.mode.DETAILED,
};
const measureMemoryExecutions = {
default: constants.measureMemory.execution.DEFAULT,
eager: constants.measureMemory.execution.EAGER,
};
function measureMemory(options = kEmptyObject) {
emitExperimentalWarning('vm.measureMemory');
validateObject(options, 'options');
const { mode = 'summary', execution = 'default' } = options;
validateOneOf(mode, 'options.mode', ['summary', 'detailed']);
validateOneOf(execution, 'options.execution', ['default', 'eager']);
const result = _measureMemory(measureMemoryModes[mode],
measureMemoryExecutions[execution]);
if (result === undefined) {
return PromiseReject(new ERR_CONTEXT_NOT_INITIALIZED());
}
return result;
}
const vmConstants = {
__proto__: null,
USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default,
DONT_CONTEXTIFY: vm_context_no_contextify,
};
ObjectFreeze(vmConstants);
module.exports = {
Script,
createContext,
createScript,
runInContext,
runInNewContext,
runInThisContext,
isContext,
compileFunction,
measureMemory,
constants: vmConstants,
};
// The vm module is patched to include vm.Module, vm.SourceTextModule
// and vm.SyntheticModule in the pre-execution phase when
// --experimental-vm-modules is on.