A (partially working) live update of node js
scripts using v8 Debug API

The problem with hot reloading of modules is very hot, at least for me. Because i think it's huge productivity boost. You change a function and the function is just changing: no restarts, no lost state. Its like modern patching updaters for HTML and CSS, many of us familiar with today, but for js.

Getting access to Debug API

nodejs has bindings for v8 Debug API as Debug object. This API provides usual debug functions with live edit along them.

You can access Debug object as v8debug.Debig running node like this:

node --debug=9999 --expose_debug_as=v8debug sha.js

Or, you can get it through vm.runInDebugContext():

const vm = require('vm');
const Debug = vm.runInDebugContext('Debug');

See nodejs docs.

I saw somewhere on the internet that this method is not reliable but i dont know is it true or not. But i know that the former method is also not 100% reliable cuz sometimes Debug object throws illegal access. Example:

let Debug = v8debug.Debug
console.log( Debug.scripts() )

$ node --debug=9999 --expose_debug_as=v8debug debug-illegal-access.js
Debugger listening on 0.0.0.0:9999

/home/ors/dev/cree/debug-illegal-access.js:2
console.log( Debug.scripts() )
                   ^
illegal access

But this works fine (node v6.3.1):

let Debug = v8debug.Debug
setImmediate(function(){
  console.log( Debug.scripts() )
})

$ node --debug=9999 --expose_debug_as=v8debug debug-no-illegal-access.js
Debugger listening on 0.0.0.0:9999
[ Script {},
  Script {},
  Script {},
  ...

In real code i use all is mostly ok through, maybe because calls to Debug API are naturally delayed by other initialization as in the last script.

Using the API to do something interesting

One can easily find Script object for any js module using code like this:

Debug.scripts().filter((s) => (
  typeof s.name == 'string' && 
  s.name.indexOf(fn)>=0 &&     // fn is our file name
  s.name.indexOf('(old') < 0   // skip old versions
))[0]

(Skipping for old versions is needed because v8 sometimes creates new script instead of patching existing. For example, if new function was added.)

We can update a source code of a script using this Debug.LiveEdit API call:

let cl = []
Debug.LiveEdit.SetScriptSource(script, code, false, cl)

(cl is array filled with actions that were done by v8 to update the script.)

Sounds easy?

Let's update something!

Consider this simple script

let code = `
  "use strict"
  let s = 'original'
  function m() {
    console.log(s)
  }
`
let code2 = `
  "use strict"
  let s = 'updated'
  function m() {
    console.log(s+'*')
  }
`    

var script = new vm.Script(code, {filename: 'test.js'})
script.runInThisContext()
m();
update('test.js', code2)
m();

Output:

original
original*

We see that the function m was patched but the scope of it is old.

(You can ask why i am finding script by filename if i have created it myself. Answer is stupid: V8 for some reasons doesnt allow one to update a script by script object returned by new vm.Script.)

An attempt to understand

To investigate the problem lets write function to list all scripts:

function list_scripts() {
  let ss = Debug.scripts()
  for (let s of ss) {
    console.log(s.id, s.type, s.compilation_type, s.name, s.is_debugger_script)
  }
}

Output:

1 0 0 undefined false
2 0 0 undefined false
3 0 0 'native prologue.js' false
4 0 0 'native runtime.js' false
5 0 0 'native v8natives.js' false
6 0 0 'native symbol.js' false
7 0 0 'native array.js' false
8 0 0 'native string.js' false
9 0 0 'native uri.js' false
10 0 0 'native math.js' false
11 0 0 'native fdlibm.js' false
12 0 0 'native regexp.js' false
13 0 0 'native arraybuffer.js' false
14 0 0 'native typedarray.js' false
15 0 0 'native iterator-prototype.js' false
16 0 0 'native generator.js' false
17 0 0 'native object-observe.js' false
18 0 0 'native collection.js' false
19 0 0 'native weak-collection.js' false
20 0 0 'native collection-iterator.js' false
21 0 0 'native promise.js' false
22 0 0 'native messages.js' false
23 0 0 'native json.js' false
24 0 0 'native array-iterator.js' false
25 0 0 'native string-iterator.js' false
26 0 0 'native templates.js' false
27 0 0 'native spread.js' false
28 0 0 'native i18n.js' false
30 0 0 'native proxy.js' false
31 0 0 'native harmony-regexp.js' false
32 0 0 'native harmony-unicode-regexps.js' false
34 0 0 'native mirrors.js' false
35 0 0 'native debug.js' false
36 0 0 'native liveedit.js' false
37 2 0 'bootstrap_node.js' false
38 2 0 'events.js' false
39 2 0 'util.js' false
40 2 0 'buffer.js' false
41 2 0 'internal/util.js' false
42 2 0 'timers.js' false
43 2 0 'internal/linkedlist.js' false
44 2 0 'assert.js' false
45 2 0 'internal/process.js' false
46 2 0 'internal/process/warning.js' false
47 2 0 'internal/process/next_tick.js' false
48 2 0 'internal/process/promises.js' false
49 2 0 'internal/process/stdio.js' false
50 2 0 'path.js' false
51 2 0 'module.js' false
52 2 0 'internal/module.js' false
53 2 0 'vm.js' false
54 2 0 'fs.js' false
55 2 0 'stream.js' false
56 2 0 '_stream_readable.js' false
57 2 0 'internal/streams/BufferList.js' false
58 2 0 '_stream_writable.js' false
59 2 0 '_stream_duplex.js' false
60 2 0 '_stream_transform.js' false
61 2 0 '_stream_passthrough.js' false
63 2 0 'console.js' false
64 2 0 'tty.js' false
65 2 0 'net.js' false
66 2 0 'internal/net.js' false
67 2 0 '/home/ors/dev/cree/sha.js' false
68 2 0 'test.js' false

68 modules! Let's output only last (with id >= 68):

68 2 0 'test.js' false

Ok. Looks better.

Now, when we place list_scripts call before and after update of the script

let script = create_and_run_script(code)
list_scripts()
m()
update('test.js', code2)
list_scripts()
m()

the output is

68 2 0 'test.js' false
original
68 2 0 'test.js' false
original*

So v8 did not create new script, it patched function in existing one. But the closure of the function still has old value of s.

It's better than nothing but it's not very convenient.

Further experiments?..

If we try to rerun the script after update (not mentioning that is not very clever idea by itself), v8 will reasonably argue that variable s is already defined. That is what strict mode is all about.

Of course we can resort to sloppy mode, but its not the way we want going ]

Here i stop for now, but you can try to improve this technology.

See also

You might be interesting in project live-node by Mihai Bazon (author of uglifyjs), that does exactly what i'am trying to accomplish here but in different approach (rewriting). As far as i understand his module works with sloppy mode, but may be it works with strict mode too or can be easily adapted.

Cheers]

 

shitpoet@gmail.com

 

free hit counters