worker: improve error (de)serialization

Rather than passing errors using some sort of string representation,
do a best effort for faithful serialization/deserialization of
uncaught exception objects.

PR-URL: https://github.com/nodejs/node/pull/20876
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Shingo Inoue <leko.noor@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
This commit is contained in:
Anna Henningsen 2017-09-26 01:42:16 +02:00
parent 6d59bfb4f8
commit d1c096cc9d
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
6 changed files with 171 additions and 16 deletions

View File

@ -0,0 +1,121 @@
'use strict';
const Buffer = require('buffer').Buffer;
const { serialize, deserialize } = require('v8');
const { SafeSet } = require('internal/safe_globals');
const kSerializedError = 0;
const kSerializedObject = 1;
const kInspectedError = 2;
const GetPrototypeOf = Object.getPrototypeOf;
const GetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const GetOwnPropertyNames = Object.getOwnPropertyNames;
const DefineProperty = Object.defineProperty;
const Assign = Object.assign;
const ObjectPrototypeToString =
Function.prototype.call.bind(Object.prototype.toString);
const ForEach = Function.prototype.call.bind(Array.prototype.forEach);
const Call = Function.prototype.call.bind(Function.prototype.call);
const errors = {
Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError
};
const errorConstructorNames = new SafeSet(Object.keys(errors));
function TryGetAllProperties(object, target = object) {
const all = Object.create(null);
if (object === null)
return all;
Assign(all, TryGetAllProperties(GetPrototypeOf(object), target));
const keys = GetOwnPropertyNames(object);
ForEach(keys, (key) => {
const descriptor = GetOwnPropertyDescriptor(object, key);
const getter = descriptor.get;
if (getter && key !== '__proto__') {
try {
descriptor.value = Call(getter, target);
} catch {}
}
if ('value' in descriptor && typeof descriptor.value !== 'function') {
delete descriptor.get;
delete descriptor.set;
all[key] = descriptor;
}
});
return all;
}
function GetConstructors(object) {
const constructors = [];
for (var current = object;
current !== null;
current = GetPrototypeOf(current)) {
const desc = GetOwnPropertyDescriptor(current, 'constructor');
if (desc && desc.value) {
DefineProperty(constructors, constructors.length, {
value: desc.value, enumerable: true
});
}
}
return constructors;
}
function GetName(object) {
const desc = GetOwnPropertyDescriptor(object, 'name');
return desc && desc.value;
}
let util;
function lazyUtil() {
if (!util)
util = require('util');
return util;
}
function serializeError(error) {
try {
if (typeof error === 'object' &&
ObjectPrototypeToString(error) === '[object Error]') {
const constructors = GetConstructors(error);
for (var i = constructors.length - 1; i >= 0; i--) {
const name = GetName(constructors[i]);
if (errorConstructorNames.has(name)) {
try { error.stack; } catch {}
const serialized = serialize({
constructor: name,
properties: TryGetAllProperties(error)
});
return Buffer.concat([Buffer.from([kSerializedError]), serialized]);
}
}
}
} catch {}
try {
const serialized = serialize(error);
return Buffer.concat([Buffer.from([kSerializedObject]), serialized]);
} catch {}
return Buffer.concat([Buffer.from([kInspectedError]),
Buffer.from(lazyUtil().inspect(error), 'utf8')]);
}
function deserializeError(error) {
switch (error[0]) {
case kSerializedError:
const { constructor, properties } = deserialize(error.subarray(1));
const ctor = errors[constructor];
return Object.create(ctor.prototype, properties);
case kSerializedObject:
return deserialize(error.subarray(1));
case kInspectedError:
const buf = Buffer.from(error.buffer,
error.byteOffset + 1,
error.byteLength - 1);
return buf.toString('utf8');
}
require('assert').fail('This should not happen');
}
module.exports = { serializeError, deserializeError };

View File

@ -1,6 +1,5 @@
'use strict';
const Buffer = require('buffer').Buffer;
const EventEmitter = require('events');
const assert = require('assert');
const path = require('path');
@ -17,6 +16,7 @@ const { internalBinding } = require('internal/bootstrap/loaders');
const { MessagePort, MessageChannel } = internalBinding('messaging');
const { handle_onclose } = internalBinding('symbols');
const { clearAsyncIdStack } = require('internal/async_hooks');
const { serializeError, deserializeError } = require('internal/error-serdes');
util.inherits(MessagePort, EventEmitter);
@ -453,17 +453,6 @@ function setupChild(evalScript) {
}
}
// TODO(addaleax): These can be improved a lot.
function serializeError(error) {
return Buffer.from(util.inspect(error), 'utf8');
}
function deserializeError(error) {
return Buffer.from(error.buffer,
error.byteOffset,
error.byteLength).toString('utf8');
}
function pipeWithoutWarning(source, dest) {
const sourceMaxListeners = source._maxListeners;
const destMaxListeners = dest._maxListeners;

View File

@ -102,6 +102,7 @@
'lib/internal/constants.js',
'lib/internal/encoding.js',
'lib/internal/errors.js',
'lib/internal/error-serdes.js',
'lib/internal/fixed_queue.js',
'lib/internal/freelist.js',
'lib/internal/fs/promises.js',

View File

@ -0,0 +1,46 @@
// Flags: --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { serializeError, deserializeError } = require('internal/error-serdes');
function cycle(err) {
return deserializeError(serializeError(err));
}
assert.strictEqual(cycle(0), 0);
assert.strictEqual(cycle(-1), -1);
assert.strictEqual(cycle(1.4), 1.4);
assert.strictEqual(cycle(null), null);
assert.strictEqual(cycle(undefined), undefined);
assert.strictEqual(cycle('foo'), 'foo');
{
const err = cycle(new Error('foo'));
assert(err instanceof Error);
assert.strictEqual(err.name, 'Error');
assert.strictEqual(err.message, 'foo');
assert(/^Error: foo\n/.test(err.stack));
}
assert.strictEqual(cycle(new RangeError('foo')).name, 'RangeError');
assert.strictEqual(cycle(new TypeError('foo')).name, 'TypeError');
assert.strictEqual(cycle(new ReferenceError('foo')).name, 'ReferenceError');
assert.strictEqual(cycle(new URIError('foo')).name, 'URIError');
assert.strictEqual(cycle(new EvalError('foo')).name, 'EvalError');
assert.strictEqual(cycle(new SyntaxError('foo')).name, 'SyntaxError');
class SubError extends Error {}
assert.strictEqual(cycle(new SubError('foo')).name, 'Error');
assert.deepStrictEqual(cycle({ message: 'foo' }), { message: 'foo' });
assert.strictEqual(cycle(Function), '[Function: Function]');
{
const err = new ERR_INVALID_ARG_TYPE('object', 'Object', 42);
assert(/^TypeError \[ERR_INVALID_ARG_TYPE\]:/.test(err));
assert.strictEqual(err.name, 'TypeError [ERR_INVALID_ARG_TYPE]');
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
}

View File

@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER) {
const w = new Worker(__filename);
w.on('message', common.mustNotCall());
w.on('error', common.mustCall((err) => {
// TODO(addaleax): be more specific here
assert(/foo/.test(err));
assert(/^Error: foo$/.test(err));
}));
} else {
setImmediate(() => {

View File

@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER) {
const w = new Worker(__filename);
w.on('message', common.mustNotCall());
w.on('error', common.mustCall((err) => {
// TODO(addaleax): be more specific here
assert(/foo/.test(err));
assert(/^Error: foo$/.test(err));
}));
} else {
throw new Error('foo');