From 39dde5e2bf3f007164dd52ef520acf1413803818 Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Sun, 24 Apr 2016 18:40:54 +0200 Subject: [PATCH 1/3] Improve error marshaling from JS to Ruby Preserve the message and the name of the error if the runtime provides them. Also, if possible use Object.assign(...) to copy any additional properties from the thrown error into the return metadata. Pack the error stack into the response value together with the error metadata. Adds :metadata reader to the Ruby error classes so that additional JS error metadata can be recovered on the Ruby side if desired. --- lib/execjs/duktape_runtime.rb | 1 + lib/execjs/external_runtime.rb | 16 +++++++++++---- lib/execjs/module.rb | 13 ++++++++++-- lib/execjs/support/encode_error.js | 12 ++++++++++++ lib/execjs/support/jsc_runner.js | 5 +++-- lib/execjs/support/jscript_runner.js | 5 +++-- lib/execjs/support/node_runner.js | 5 +++-- lib/execjs/support/spidermonkey_runner.js | 5 +++-- lib/execjs/support/v8_runner.js | 5 +++-- test/test_execjs.rb | 24 +++++++++++++++++++++++ 10 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 lib/execjs/support/encode_error.js diff --git a/lib/execjs/duktape_runtime.rb b/lib/execjs/duktape_runtime.rb index 8ae42a5..02792af 100644 --- a/lib/execjs/duktape_runtime.rb +++ b/lib/execjs/duktape_runtime.rb @@ -46,6 +46,7 @@ def wrap_error(e) re = / \(line (\d+)\)$/ lineno = e.message[re, 1] || 1 error = klass.new(e.message.sub(re, "")) + error.metadata = {} # FIXME: has to be available upstream error.set_backtrace(["(execjs):#{lineno}"] + e.backtrace) error else diff --git a/lib/execjs/external_runtime.rb b/lib/execjs/external_runtime.rb index 9a4b493..4d0768d 100644 --- a/lib/execjs/external_runtime.rb +++ b/lib/execjs/external_runtime.rb @@ -65,11 +65,12 @@ def write_to_tempfile(contents) end def extract_result(output, filename) - status, value, stack = output.empty? ? [] : ::JSON.parse(output, create_additions: false) + status, value= output.empty? ? [] : ::JSON.parse(output, create_additions: false) if status == "ok" value else - stack ||= "" + demarshaled_err = value + stack = demarshaled_err['stack'] || "" real_filename = File.realpath(filename) stack = stack.split("\n").map do |line| line.sub(" at ", "") @@ -79,8 +80,11 @@ def extract_result(output, filename) end stack.reject! { |line| ["eval code", "eval@[native code]"].include?(line) } stack.shift unless stack[0].to_s.include?("(execjs)") - error_class = value =~ /SyntaxError:/ ? RuntimeError : ProgramError - error = error_class.new(value) + + error_class = demarshaled_err['name'].to_s =~ /SyntaxError/ ? RuntimeError : ProgramError + + error = error_class.new(demarshaled_err.delete('message').to_s) + error.metadata = demarshaled_err error.set_backtrace(stack + caller) raise error end @@ -156,6 +160,10 @@ def json2_source @json2_source ||= IO.read(ExecJS.root + "/support/json2.js") end + def encode_error_source + @encode_error_source ||= IO.read(ExecJS.root + "/support/encode_error.js") + end + def encode_source(source) encoded_source = encode_unicode_codepoints(source) ::JSON.generate("(function(){ #{encoded_source} })()", quirks_mode: true) diff --git a/lib/execjs/module.rb b/lib/execjs/module.rb index b89dc22..49deabc 100644 --- a/lib/execjs/module.rb +++ b/lib/execjs/module.rb @@ -3,8 +3,17 @@ module ExecJS class Error < ::StandardError; end - class RuntimeError < Error; end - class ProgramError < Error; end + + class RuntimeError < Error + # Stores the unmarshaled JavaScript error information if it was available (type, message etc.) + attr_accessor :metadata + end + + class ProgramError < Error + # Stores the unmarshaled JavaScript error information if it was available (type, message etc.) + attr_accessor :metadata + end + class RuntimeUnavailable < RuntimeError; end class << self diff --git a/lib/execjs/support/encode_error.js b/lib/execjs/support/encode_error.js new file mode 100644 index 0000000..411ed82 --- /dev/null +++ b/lib/execjs/support/encode_error.js @@ -0,0 +1,12 @@ +function encodeError(err) { + var errMeta = {type: typeof(err), name: null, message: '' + err, stack: null}; + if (typeof(err) === 'object') { + // Copy "message" and "name" since those are standard attributes for an Error + if (typeof(err.name) !== 'undefined') errMeta.name = err.name; + if (typeof(err.message) !== 'undefined') errMeta.message = err.message; + if (typeof(err.stack) !== 'undefined') errMeta.stack = err.stack; + // Copy all of the other properties that are tacked on the Error itself + if (typeof(Object.assign) === 'function') Object.assign(errMeta, err); + } + return errMeta; +}; diff --git a/lib/execjs/support/jsc_runner.js b/lib/execjs/support/jsc_runner.js index c57a944..c9fa856 100644 --- a/lib/execjs/support/jsc_runner.js +++ b/lib/execjs/support/jsc_runner.js @@ -1,5 +1,6 @@ (function(program, execJS) { execJS(program) })(function() { #{source} }, function(program) { + #{encode_error_source} var output; try { result = program(); @@ -9,10 +10,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } } } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } }); diff --git a/lib/execjs/support/jscript_runner.js b/lib/execjs/support/jscript_runner.js index fc92b4a..e24f683 100644 --- a/lib/execjs/support/jscript_runner.js +++ b/lib/execjs/support/jscript_runner.js @@ -2,6 +2,7 @@ return eval(#{encode_source(source)}); }, function(program) { #{json2_source} + #{encode_error_source} var output, print = function(string) { WScript.Echo(string); }; @@ -13,10 +14,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print(JSON.stringify(['err', err.name + ': ' + err.message, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } } } catch (err) { - print(JSON.stringify(['err', err.name + ': ' + err.message, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } }); diff --git a/lib/execjs/support/node_runner.js b/lib/execjs/support/node_runner.js index ab0c6ec..7b26bad 100644 --- a/lib/execjs/support/node_runner.js +++ b/lib/execjs/support/node_runner.js @@ -3,6 +3,7 @@ var output, print = function(string) { process.stdout.write('' + string); }; + #{encode_error_source} try { result = program(); if (typeof result == 'undefined' && result !== null) { @@ -11,10 +12,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } } } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } }); diff --git a/lib/execjs/support/spidermonkey_runner.js b/lib/execjs/support/spidermonkey_runner.js index c57a944..c9fa856 100644 --- a/lib/execjs/support/spidermonkey_runner.js +++ b/lib/execjs/support/spidermonkey_runner.js @@ -1,5 +1,6 @@ (function(program, execJS) { execJS(program) })(function() { #{source} }, function(program) { + #{encode_error_source} var output; try { result = program(); @@ -9,10 +10,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } } } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } }); diff --git a/lib/execjs/support/v8_runner.js b/lib/execjs/support/v8_runner.js index c57a944..c9fa856 100644 --- a/lib/execjs/support/v8_runner.js +++ b/lib/execjs/support/v8_runner.js @@ -1,5 +1,6 @@ (function(program, execJS) { execJS(program) })(function() { #{source} }, function(program) { + #{encode_error_source} var output; try { result = program(); @@ -9,10 +10,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } } } catch (err) { - print(JSON.stringify(['err', '' + err, err.stack])); + print(JSON.stringify(['err', encodeError(err)])); } }); diff --git a/test/test_execjs.rb b/test/test_execjs.rb index 7fd967f..bbe743f 100644 --- a/test/test_execjs.rb +++ b/test/test_execjs.rb @@ -271,6 +271,30 @@ def test_exec_syntax_error end end + def test_exec_custom_js_error + begin + ExecJS.exec('CustomError = function(message, line, column) { + this.name = "CustomError"; + this.message = message; + this.line = line; + this.column = column; + this.specialProperty = 123; + }; + CustomError.prototype = Error.prototype; + throw new CustomError("this has failed", 10, 15) + ') + flunk + rescue ExecJS::ProgramError => e + assert e + assert_match /this has failed/, e.message + + meta = e.metadata + assert_equal 'object', meta['type'] + assert_equal 'CustomError', meta['name'] + assert_equal 123, meta['specialProperty'] + end + end + def test_eval_syntax_error begin ExecJS.eval(")") From 01605a4556aa8c6cb960e631d71ca14c2ec95bde Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Sun, 24 Apr 2016 22:02:30 +0200 Subject: [PATCH 2/3] Check for Node version when running Travis builds --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8c0ef70..4760e01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ sudo: false before_install: - if [ "$EXECJS_RUNTIME" == "V8" ]; then brew update; fi - if [ "$EXECJS_RUNTIME" == "V8" ]; then brew install v8; fi + - if [ "$EXECJS_RUNTIME" == "Node" ]; then echo node --version; fi script: bundle exec ruby test/test_execjs.rb matrix: From ebf997f8afdec036e4eed83acbcc026477f785ae Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Sun, 24 Apr 2016 22:20:23 +0200 Subject: [PATCH 3/3] Another try at printing nodejs version --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4760e01..1532611 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,9 @@ sudo: false before_install: - if [ "$EXECJS_RUNTIME" == "V8" ]; then brew update; fi - if [ "$EXECJS_RUNTIME" == "V8" ]; then brew install v8; fi - - if [ "$EXECJS_RUNTIME" == "Node" ]; then echo node --version; fi -script: bundle exec ruby test/test_execjs.rb +script: + - node --version + - bundle exec ruby test/test_execjs.rb matrix: include: