1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22'use strict';
23
24const {
25  ArrayPrototypeForEach,
26  Symbol,
27  PromiseReject,
28  ReflectApply,
29} = primordials;
30
31const {
32  ContextifyScript,
33  MicrotaskQueue,
34  makeContext,
35  constants,
36  measureMemory: _measureMemory,
37} = internalBinding('contextify');
38const {
39  ERR_CONTEXT_NOT_INITIALIZED,
40  ERR_INVALID_ARG_TYPE,
41} = require('internal/errors').codes;
42const {
43  validateArray,
44  validateBoolean,
45  validateBuffer,
46  validateInt32,
47  validateOneOf,
48  validateObject,
49  validateString,
50  validateStringArray,
51  validateUint32,
52} = require('internal/validators');
53const {
54  emitExperimentalWarning,
55  kEmptyObject,
56  kVmBreakFirstLineSymbol,
57} = require('internal/util');
58const {
59  getHostDefinedOptionId,
60  internalCompileFunction,
61  isContext,
62  registerImportModuleDynamically,
63} = require('internal/vm');
64const kParsingContext = Symbol('script parsing context');
65
66class Script extends ContextifyScript {
67  constructor(code, options = kEmptyObject) {
68    code = `${code}`;
69    if (typeof options === 'string') {
70      options = { filename: options };
71    } else {
72      validateObject(options, 'options');
73    }
74
75    const {
76      filename = 'evalmachine.<anonymous>',
77      lineOffset = 0,
78      columnOffset = 0,
79      cachedData,
80      produceCachedData = false,
81      importModuleDynamically,
82      [kParsingContext]: parsingContext,
83    } = options;
84
85    validateString(filename, 'options.filename');
86    validateInt32(lineOffset, 'options.lineOffset');
87    validateInt32(columnOffset, 'options.columnOffset');
88    if (cachedData !== undefined) {
89      validateBuffer(cachedData, 'options.cachedData');
90    }
91    validateBoolean(produceCachedData, 'options.produceCachedData');
92
93    const hostDefinedOptionId =
94        getHostDefinedOptionId(importModuleDynamically, filename);
95    // Calling `ReThrow()` on a native TryCatch does not generate a new
96    // abort-on-uncaught-exception check. A dummy try/catch in JS land
97    // protects against that.
98    try { // eslint-disable-line no-useless-catch
99      super(code,
100            filename,
101            lineOffset,
102            columnOffset,
103            cachedData,
104            produceCachedData,
105            parsingContext,
106            hostDefinedOptionId);
107    } catch (e) {
108      throw e; /* node-do-not-add-exception-line */
109    }
110
111    if (importModuleDynamically !== undefined) {
112      registerImportModuleDynamically(this, importModuleDynamically);
113    }
114  }
115
116  runInThisContext(options) {
117    const { breakOnSigint, args } = getRunInContextArgs(null, options);
118    if (breakOnSigint && process.listenerCount('SIGINT') > 0) {
119      return sigintHandlersWrap(super.runInContext, this, args);
120    }
121    return ReflectApply(super.runInContext, this, args);
122  }
123
124  runInContext(contextifiedObject, options) {
125    validateContext(contextifiedObject);
126    const { breakOnSigint, args } = getRunInContextArgs(
127      contextifiedObject,
128      options,
129    );
130    if (breakOnSigint && process.listenerCount('SIGINT') > 0) {
131      return sigintHandlersWrap(super.runInContext, this, args);
132    }
133    return ReflectApply(super.runInContext, this, args);
134  }
135
136  runInNewContext(contextObject, options) {
137    const context = createContext(contextObject, getContextOptions(options));
138    return this.runInContext(context, options);
139  }
140}
141
142function validateContext(contextifiedObject) {
143  if (!isContext(contextifiedObject)) {
144    throw new ERR_INVALID_ARG_TYPE('contextifiedObject', 'vm.Context',
145                                   contextifiedObject);
146  }
147}
148
149function getRunInContextArgs(contextifiedObject, options = kEmptyObject) {
150  validateObject(options, 'options');
151
152  let timeout = options.timeout;
153  if (timeout === undefined) {
154    timeout = -1;
155  } else {
156    validateUint32(timeout, 'options.timeout', true);
157  }
158
159  const {
160    displayErrors = true,
161    breakOnSigint = false,
162    [kVmBreakFirstLineSymbol]: breakFirstLine = false,
163  } = options;
164
165  validateBoolean(displayErrors, 'options.displayErrors');
166  validateBoolean(breakOnSigint, 'options.breakOnSigint');
167
168  return {
169    breakOnSigint,
170    args: [
171      contextifiedObject,
172      timeout,
173      displayErrors,
174      breakOnSigint,
175      breakFirstLine,
176    ],
177  };
178}
179
180function getContextOptions(options) {
181  if (!options)
182    return {};
183  const contextOptions = {
184    name: options.contextName,
185    origin: options.contextOrigin,
186    codeGeneration: undefined,
187    microtaskMode: options.microtaskMode,
188  };
189  if (contextOptions.name !== undefined)
190    validateString(contextOptions.name, 'options.contextName');
191  if (contextOptions.origin !== undefined)
192    validateString(contextOptions.origin, 'options.contextOrigin');
193  if (options.contextCodeGeneration !== undefined) {
194    validateObject(options.contextCodeGeneration,
195                   'options.contextCodeGeneration');
196    const { strings, wasm } = options.contextCodeGeneration;
197    if (strings !== undefined)
198      validateBoolean(strings, 'options.contextCodeGeneration.strings');
199    if (wasm !== undefined)
200      validateBoolean(wasm, 'options.contextCodeGeneration.wasm');
201    contextOptions.codeGeneration = { strings, wasm };
202  }
203  if (options.microtaskMode !== undefined)
204    validateString(options.microtaskMode, 'options.microtaskMode');
205  return contextOptions;
206}
207
208let defaultContextNameIndex = 1;
209function createContext(contextObject = {}, options = kEmptyObject) {
210  if (isContext(contextObject)) {
211    return contextObject;
212  }
213
214  validateObject(options, 'options');
215
216  const {
217    name = `VM Context ${defaultContextNameIndex++}`,
218    origin,
219    codeGeneration,
220    microtaskMode,
221  } = options;
222
223  validateString(name, 'options.name');
224  if (origin !== undefined)
225    validateString(origin, 'options.origin');
226  if (codeGeneration !== undefined)
227    validateObject(codeGeneration, 'options.codeGeneration');
228
229  let strings = true;
230  let wasm = true;
231  if (codeGeneration !== undefined) {
232    ({ strings = true, wasm = true } = codeGeneration);
233    validateBoolean(strings, 'options.codeGeneration.strings');
234    validateBoolean(wasm, 'options.codeGeneration.wasm');
235  }
236
237  validateOneOf(microtaskMode,
238                'options.microtaskMode',
239                ['afterEvaluate', undefined]);
240  const microtaskQueue = microtaskMode === 'afterEvaluate' ?
241    new MicrotaskQueue() :
242    null;
243
244  makeContext(contextObject, name, origin, strings, wasm, microtaskQueue);
245  return contextObject;
246}
247
248function createScript(code, options) {
249  return new Script(code, options);
250}
251
252// Remove all SIGINT listeners and re-attach them after the wrapped function
253// has executed, so that caught SIGINT are handled by the listeners again.
254function sigintHandlersWrap(fn, thisArg, argsArray) {
255  const sigintListeners = process.rawListeners('SIGINT');
256
257  process.removeAllListeners('SIGINT');
258
259  try {
260    return ReflectApply(fn, thisArg, argsArray);
261  } finally {
262    // Add using the public methods so that the `newListener` handler of
263    // process can re-attach the listeners.
264    ArrayPrototypeForEach(sigintListeners, (listener) => {
265      process.addListener('SIGINT', listener);
266    });
267  }
268}
269
270function runInContext(code, contextifiedObject, options) {
271  validateContext(contextifiedObject);
272  if (typeof options === 'string') {
273    options = {
274      filename: options,
275      [kParsingContext]: contextifiedObject,
276    };
277  } else {
278    options = { ...options, [kParsingContext]: contextifiedObject };
279  }
280  return createScript(code, options)
281    .runInContext(contextifiedObject, options);
282}
283
284function runInNewContext(code, contextObject, options) {
285  if (typeof options === 'string') {
286    options = { filename: options };
287  }
288  contextObject = createContext(contextObject, getContextOptions(options));
289  options = { ...options, [kParsingContext]: contextObject };
290  return createScript(code, options).runInNewContext(contextObject, options);
291}
292
293function runInThisContext(code, options) {
294  if (typeof options === 'string') {
295    options = { filename: options };
296  }
297  return createScript(code, options).runInThisContext(options);
298}
299
300function compileFunction(code, params, options = kEmptyObject) {
301  validateString(code, 'code');
302  if (params !== undefined) {
303    validateStringArray(params, 'params');
304  }
305  const {
306    filename = '',
307    columnOffset = 0,
308    lineOffset = 0,
309    cachedData = undefined,
310    produceCachedData = false,
311    parsingContext = undefined,
312    contextExtensions = [],
313    importModuleDynamically,
314  } = options;
315
316  validateString(filename, 'options.filename');
317  validateInt32(columnOffset, 'options.columnOffset');
318  validateInt32(lineOffset, 'options.lineOffset');
319  if (cachedData !== undefined)
320    validateBuffer(cachedData, 'options.cachedData');
321  validateBoolean(produceCachedData, 'options.produceCachedData');
322  if (parsingContext !== undefined) {
323    if (
324      typeof parsingContext !== 'object' ||
325      parsingContext === null ||
326      !isContext(parsingContext)
327    ) {
328      throw new ERR_INVALID_ARG_TYPE(
329        'options.parsingContext',
330        'Context',
331        parsingContext,
332      );
333    }
334  }
335  validateArray(contextExtensions, 'options.contextExtensions');
336  ArrayPrototypeForEach(contextExtensions, (extension, i) => {
337    const name = `options.contextExtensions[${i}]`;
338    validateObject(extension, name, { __proto__: null, nullable: true });
339  });
340
341  const hostDefinedOptionId =
342      getHostDefinedOptionId(importModuleDynamically, filename);
343
344  return internalCompileFunction(
345    code, filename, lineOffset, columnOffset,
346    cachedData, produceCachedData, parsingContext, contextExtensions,
347    params, hostDefinedOptionId, importModuleDynamically,
348  ).function;
349}
350
351const measureMemoryModes = {
352  summary: constants.measureMemory.mode.SUMMARY,
353  detailed: constants.measureMemory.mode.DETAILED,
354};
355
356const measureMemoryExecutions = {
357  default: constants.measureMemory.execution.DEFAULT,
358  eager: constants.measureMemory.execution.EAGER,
359};
360
361function measureMemory(options = kEmptyObject) {
362  emitExperimentalWarning('vm.measureMemory');
363  validateObject(options, 'options');
364  const { mode = 'summary', execution = 'default' } = options;
365  validateOneOf(mode, 'options.mode', ['summary', 'detailed']);
366  validateOneOf(execution, 'options.execution', ['default', 'eager']);
367  const result = _measureMemory(measureMemoryModes[mode],
368                                measureMemoryExecutions[execution]);
369  if (result === undefined) {
370    return PromiseReject(new ERR_CONTEXT_NOT_INITIALIZED());
371  }
372  return result;
373}
374
375module.exports = {
376  Script,
377  createContext,
378  createScript,
379  runInContext,
380  runInNewContext,
381  runInThisContext,
382  isContext,
383  compileFunction,
384  measureMemory,
385};
386
387// The vm module is patched to include vm.Module, vm.SourceTextModule
388// and vm.SyntheticModule in the pre-execution phase when
389// --experimental-vm-modules is on.
390