Add FetchAPI support and fix loop race [upd] #289 (#293)

This PR adds Fetch API and fixes #289 by using concurrency safe Otto VM wrapper wherever it's possible. This involves new package geth/jail/vm that is used by jail and by our forked ottoext/{fetch/timers/loop} packages.

It also adds more tests that are supposed to be run with --race flag of go test.
This commit is contained in:
Ivan Daniluk 2017-09-08 13:55:17 +02:00 committed by Ivan Tomilov
parent 39aeb3b09d
commit 6a096607cf
40 changed files with 838 additions and 602 deletions

View File

@ -255,10 +255,15 @@ type TxQueueManager interface {
}
// JailCell represents single jail cell, which is basically a JavaScript VM.
// It's designed to be a transparent wrapper around otto.VM's methods.
type JailCell interface {
// Set a value inside VM.
Set(string, interface{}) error
// Get a value from VM.
Get(string) (otto.Value, error)
Run(string) (otto.Value, error)
// Run an arbitrary JS code. Input maybe string or otto.Script.
Run(interface{}) (otto.Value, error)
// Call an arbitrary JS function by name and args.
Call(item string, this interface{}, args ...interface{}) (otto.Value, error)
}

View File

@ -1,15 +1,15 @@
# Package jail
# Jail
--
import "github.com/status-im/status-go/geth/jail"
[![GoDoc](https://godoc.org/github.com/status-im/status-go/geth/jail?status.svg)](https://godoc.org/github.com/status-im/status-go/geth/jail)
go:generate godocdown -heading Title -o README.md
Package jail implements "jailed" enviroment for executing arbitrary JavaScript
code using Otto JS interpreter (https://github.com/robertkrimen/otto).
Jail create multiple JailCells, one cell per status client chat. Each cell runs
own Otto virtual machine and lives forever, but that may change in the future.
Jail create multiple Cells, one cell per status client chat. Each cell runs own
Otto virtual machine and lives forever, but that may change in the future.
+----------------------------------------------+
| Jail |
@ -24,28 +24,34 @@ own Otto virtual machine and lives forever, but that may change in the future.
++-------++ ++-------++ ++-------++ ++-------++
# Get and Set
### Cells
(*JailCell).Get/Set functions provide transparent and concurrently safe wrappers
for Otto VM Get and Set functions respectively. See Otto documentation for usage
Each Cell object embeds *VM from 'jail/vm' for concurrency safe wrapper around
*otto.VM functions. This important when dealing with setTimeout and Fetch API
functions (see below).
### Get and Set
(*VM).Get/Set functions provide transparent and concurrently safe wrappers for
Otto VM Get and Set functions respectively. See Otto documentation for usage
examples: https://godoc.org/github.com/robertkrimen/otto
# Call and Run
### Call and Run
(*JailCell).Call/Run functions allows executing arbitrary JS in the cell.
They're also wrappers arount Otto VM functions of the same name. Run accepts raw
JS strings for execution, Call takes a JS function name (defined in VM) and
parameters.
(*VM).Call/Run functions allows executing arbitrary JS in the cell. They're also
wrappers arount Otto VM functions of the same name. Run accepts raw JS strings
for execution, Call takes a JS function name (defined in VM) and parameters.
# Timeouts and intervals support
### Timeouts and intervals support
Default Otto VM interpreter doesn't support setTimeout()/setInterval() JS
functions, because they're not part of ECMA-262 spec, but properties of the
window object in browser. We add support for them using
http://github.com/deoxxa/ottoext/timers and
http://github.com/deoxxa/ottoext/loop packages.
http://github.com/status-im/ottoext/timers and
http://github.com/status-im/ottoext/loop packages.
Each cell starts a new loop in a separate goroutine, registers functions for
setTimeout/setInterval calls and associate them with this loop. All JS code
@ -67,6 +73,26 @@ In order to capture response one may use following approach:
cell.Run(`setTimeout(function(){ __captureResponse("OK") }, 2000);`)
# Fetch support
### Fetch support
### TBD
Fetch API is implemented using http://github.com/status-im/ottoext/fetch
package. When Cell is created, corresponding handlers are registered within VM
and associated event loop.
Due to asynchronous nature of Fetch API, the following code will return
immediately:
cell.Run(`fetch('http://example.com/').then(function(data) { ... })`)
### and callback function in a promise will be executed in a event loop in the
backgrounds. Thus, it's user responsibility to register a corresponding callback
function before:
cell.Set("__captureSuccess", func(res otto.Value) { ... })
cell.Run(`fetch('http://example.com').then(function(r) {
return r.text()
}).then(function(data) {
// user code
__captureSuccess(data)
}))

View File

@ -1,8 +1,9 @@
//go:generate godocdown -heading Title -o README.md
/*
Package jail implements "jailed" enviroment for executing arbitrary
JavaScript code using Otto JS interpreter (https://github.com/robertkrimen/otto).
Jail create multiple JailCells, one cell per status client chat. Each cell runs own
Jail create multiple Cells, one cell per status client chat. Each cell runs own
Otto virtual machine and lives forever, but that may change in the future.
+----------------------------------------------+
@ -17,16 +18,21 @@ Otto virtual machine and lives forever, but that may change in the future.
|| Loop || || Loop || || Loop || || Loop ||
++-------++ ++-------++ ++-------++ ++-------++
Cells
Each Cell object embeds *VM from 'jail/vm' for concurrency safe wrapper around
*otto.VM functions. This important when dealing with setTimeout and Fetch API
functions (see below).
Get and Set
(*JailCell).Get/Set functions provide transparent and concurrently safe wrappers for
(*VM).Get/Set functions provide transparent and concurrently safe wrappers for
Otto VM Get and Set functions respectively. See Otto documentation for usage examples:
https://godoc.org/github.com/robertkrimen/otto
Call and Run
(*JailCell).Call/Run functions allows executing arbitrary JS in the cell. They're also
(*VM).Call/Run functions allows executing arbitrary JS in the cell. They're also
wrappers arount Otto VM functions of the same name. Run accepts raw JS strings for execution,
Call takes a JS function name (defined in VM) and parameters.
@ -34,7 +40,7 @@ Timeouts and intervals support
Default Otto VM interpreter doesn't support setTimeout()/setInterval() JS functions,
because they're not part of ECMA-262 spec, but properties of the window object in browser.
We add support for them using http://github.com/deoxxa/ottoext/timers and http://github.com/deoxxa/ottoext/loop
We add support for them using http://github.com/status-im/ottoext/timers and http://github.com/status-im/ottoext/loop
packages.
Each cell starts a new loop in a separate goroutine, registers functions for setTimeout/setInterval
@ -58,6 +64,24 @@ In order to capture response one may use following approach:
Fetch support
TBD
Fetch API is implemented using http://github.com/status-im/ottoext/fetch package. When
Cell is created, corresponding handlers are registered within VM and associated event loop.
Due to asynchronous nature of Fetch API, the following code will return immediately:
cell.Run(`fetch('http://example.com/').then(function(data) { ... })`)
and callback function in a promise will be executed in a event loop in the backgrounds. Thus,
it's user responsibility to register a corresponding callback function before:
cell.Set("__captureSuccess", func(res otto.Value) { ... })
cell.Run(`fetch('http://example.com').then(function(r) {
return r.text()
}).then(function(data) {
// user code
__captureSuccess(data)
}))
*/
package jail

View File

@ -1,7 +1,9 @@
ottoext
=======
[![GoDoc](https://godoc.org/github.com/status-im/ottoext?status.svg)](https://godoc.org/github.com/status-im/ottoext)
Originally based on [github.com/deoxxa/ottoext](https://github.com/deoxxa/ottoext)
[![GoDoc](https://godoc.org/github.com/status-im/status-go/geth/jail/ottoext?status.svg)](https://godoc.org/github.com/status-im/status-go/geth/jail/ottoext)
Overview
--------

View File

@ -10,8 +10,9 @@ import (
"github.com/GeertJohan/go.rice"
"github.com/robertkrimen/otto"
"github.com/status-im/ottoext/loop"
"github.com/status-im/ottoext/promise"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/promise"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
func mustValue(v otto.Value, err error) otto.Value {
@ -36,7 +37,7 @@ type fetchTask struct {
func (t *fetchTask) SetID(id int64) { t.id = id }
func (t *fetchTask) GetID() int64 { return t.id }
func (t *fetchTask) Execute(vm *otto.Otto, l *loop.Loop) error {
func (t *fetchTask) Execute(vm *vm.VM, l *loop.Loop) error {
var arguments []interface{}
if t.err != nil {
@ -48,6 +49,12 @@ func (t *fetchTask) Execute(vm *otto.Otto, l *loop.Loop) error {
arguments = append(arguments, e)
}
// We're locking on VM here because underlying otto's VM
// is not concurrently safe, and this function indirectly
// access vm's functions in cb.Call/h.Set.
vm.Lock()
defer vm.Unlock()
t.jsRes.Set("status", t.status)
t.jsRes.Set("statusText", t.statusText)
h := mustValue(t.jsRes.Get("headers")).Object()
@ -70,11 +77,11 @@ func (t *fetchTask) Execute(vm *otto.Otto, l *loop.Loop) error {
func (t *fetchTask) Cancel() {
}
func Define(vm *otto.Otto, l *loop.Loop) error {
func Define(vm *vm.VM, l *loop.Loop) error {
return DefineWithHandler(vm, l, nil)
}
func DefineWithHandler(vm *otto.Otto, l *loop.Loop, h http.Handler) error {
func DefineWithHandler(vm *vm.VM, l *loop.Loop, h http.Handler) error {
if err := promise.Define(vm, l); err != nil {
return err
}

View File

@ -0,0 +1,222 @@
package fetch_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/jail/internal/fetch"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/vm"
"github.com/stretchr/testify/suite"
)
func (s *FetchSuite) TestFetch() {
ch := make(chan struct{})
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
ch <- struct{}{}
})
err := fetch.Define(s.vm, s.loop)
s.NoError(err)
err = s.loop.Eval(`fetch('` + s.srv.URL + `').then(function(r) {
return r.text();
}).then(function(d) {
if (d.indexOf('hellox') === -1) {
throw new Error('what');
}
});`)
s.NoError(err)
select {
case <-ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
}
}
func (s *FetchSuite) TestFetchCallback() {
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
err := fetch.Define(s.vm, s.loop)
s.NoError(err)
ch := make(chan struct{})
err = s.vm.Set("__capture", func(str string) {
s.Contains(str, "hello")
ch <- struct{}{}
})
s.NoError(err)
err = s.loop.Eval(`fetch('` + s.srv.URL + `').then(function(r) {
return r.text();
}).then(__capture)`)
s.NoError(err)
select {
case <-ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
}
}
func (s *FetchSuite) TestFetchHeaders() {
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("header-one", "1")
w.Header().Add("header-two", "2a")
w.Header().Add("header-two", "2b")
w.Write([]byte("hello"))
})
err := fetch.Define(s.vm, s.loop)
s.NoError(err)
ch := make(chan struct{})
err = s.vm.Set("__capture", func(str string) {
s.Equal(str, `{"header-one":["1"],"header-two":["2a","2b"]}`)
ch <- struct{}{}
})
s.NoError(err)
err = s.loop.Eval(`fetch('` + s.srv.URL + `').then(function(r) {
return __capture(JSON.stringify({
'header-one': r.headers.getAll('header-one'),
'header-two': r.headers.getAll('header-two'),
}));
})`)
s.NoError(err)
select {
case <-ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
}
}
func (s *FetchSuite) TestFetchJSON() {
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// these spaces are here so we can disambiguate between this and the
// re-encoded data the javascript below spits out
w.Write([]byte("[ 1 , 2 , 3 ]"))
})
err := fetch.Define(s.vm, s.loop)
s.NoError(err)
ch := make(chan struct{})
err = s.vm.Set("__capture", func(str string) {
s.Equal(str, `[1,2,3]`)
ch <- struct{}{}
})
s.NoError(err)
err = s.loop.Eval(`fetch('` + s.srv.URL + `').then(function(r) { return r.json(); }).then(function(d) {
return setTimeout(__capture, 4, JSON.stringify(d));
})`)
s.NoError(err)
select {
case <-ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
}
}
func (s *FetchSuite) TestFetchWithHandler() {
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// these spaces are here so we can disambiguate between this and the
// re-encoded data the javascript below spits out
w.Write([]byte("[ 1 , 2 , 3 ]"))
})
err := fetch.DefineWithHandler(s.vm, s.loop, s.mux)
s.NoError(err)
ch := make(chan struct{})
err = s.vm.Set("__capture", func(str string) {
s.Equal(str, `[1,2,3]`)
ch <- struct{}{}
})
s.NoError(err)
err = s.loop.Eval(`fetch('/').then(function(r) { return r.json(); }).then(function(d) {
return setTimeout(__capture, 4, JSON.stringify(d));
})`)
s.NoError(err)
select {
case <-ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
}
}
func (s *FetchSuite) TestFetchWithHandlerParallel() {
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
err := fetch.DefineWithHandler(s.vm, s.loop, s.mux)
s.NoError(err)
ch := make(chan struct{})
err = s.vm.Set("__capture", func(c otto.FunctionCall) otto.Value {
ch <- struct{}{}
return otto.UndefinedValue()
})
s.NoError(err)
err = s.loop.Eval(`Promise.all([1,2,3,4,5].map(function(i) { return fetch('/' + i).then(__capture); }))`)
s.NoError(err)
timerCh := time.After(1 * time.Second)
var count int
loop:
for i := 0; i < 5; i++ {
select {
case <-ch:
count++
case <-timerCh:
break loop
}
}
s.Equal(5, count)
}
type FetchSuite struct {
suite.Suite
mux *http.ServeMux
srv *httptest.Server
loop *loop.Loop
vm *vm.VM
}
func (s *FetchSuite) SetupTest() {
s.mux = http.NewServeMux()
s.srv = httptest.NewServer(s.mux)
o := otto.New()
s.vm = vm.New(o)
s.loop = loop.New(s.vm)
go s.loop.Run()
}
func (s *FetchSuite) TearDownSuite() {
s.srv.Close()
}
func TestFetchSuite(t *testing.T) {
suite.Run(t, new(FetchSuite))
}

View File

@ -5,7 +5,7 @@ import (
"sync"
"sync/atomic"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
func formatTask(t Task) string {
@ -29,7 +29,7 @@ func formatTask(t Task) string {
type Task interface {
SetID(id int64)
GetID() int64
Execute(vm *otto.Otto, l *Loop) error
Execute(vm *vm.VM, l *Loop) error
Cancel()
}
@ -39,7 +39,7 @@ type Task interface {
// to finalise on the VM. The channel holding the tasks pending finalising can
// be buffered or unbuffered.
type Loop struct {
vm *otto.Otto
vm *vm.VM
id int64
lock sync.RWMutex
tasks map[int64]Task
@ -48,13 +48,13 @@ type Loop struct {
}
// New creates a new Loop with an unbuffered ready queue on a specific VM.
func New(vm *otto.Otto) *Loop {
func New(vm *vm.VM) *Loop {
return NewWithBacklog(vm, 0)
}
// NewWithBacklog creates a new Loop on a specific VM, giving it a buffered
// queue, the capacity of which being specified by the backlog argument.
func NewWithBacklog(vm *otto.Otto, backlog int) *Loop {
func NewWithBacklog(vm *vm.VM, backlog int) *Loop {
return &Loop{
vm: vm,
tasks: make(map[int64]Task),
@ -62,10 +62,8 @@ func NewWithBacklog(vm *otto.Otto, backlog int) *Loop {
}
}
// VM gets the JavaScript interpreter associated with the loop. This will be
// some kind of Otto object, but it's wrapped in an interface so the
// `ottoext` library can work with forks/extensions of otto.
func (l *Loop) VM() *otto.Otto {
// VM gets the JavaScript interpreter associated with the loop.
func (l *Loop) VM() *vm.VM {
return l.vm
}
@ -107,15 +105,6 @@ func (l *Loop) Ready(t Task) {
l.ready <- t
}
// EvalAndRun is a combination of Eval and Run. Creatively named.
func (l *Loop) EvalAndRun(s interface{}) error {
if err := l.Eval(s); err != nil {
return err
}
return l.Run()
}
// Eval executes some code in the VM associated with the loop and returns an
// error if that execution fails.
func (l *Loop) Eval(s interface{}) error {

View File

@ -3,8 +3,9 @@ package looptask
import (
"errors"
"github.com/status-im/ottoext/loop"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
// IdleTask is designed to sit in a loop and keep it active, without doing any
@ -29,7 +30,7 @@ func (i IdleTask) Cancel() {}
// Execute always returns an error for an IdleTask, as it should never
// actually be run.
func (i IdleTask) Execute(vm *otto.Otto, l *loop.Loop) error {
func (i IdleTask) Execute(vm *vm.VM, l *loop.Loop) error {
return errors.New("Idle task should never execute")
}
@ -65,7 +66,7 @@ func (e EvalTask) Cancel() {}
// Execute runs the EvalTask's otto.Script in the vm provided, pushing the
// resultant return value and error (or nil) into the associated channels.
// If the execution results in an error, it will return that error.
func (e EvalTask) Execute(vm *otto.Otto, l *loop.Loop) error {
func (e EvalTask) Execute(vm *vm.VM, l *loop.Loop) error {
v, err := vm.Run(e.Script)
e.Value <- v
e.Error <- err

View File

@ -1,13 +1,12 @@
package promise
import (
"github.com/robertkrimen/otto"
"github.com/status-im/ottoext/loop"
"github.com/status-im/ottoext/timers"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/timers"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
func Define(vm *otto.Otto, l *loop.Loop) error {
func Define(vm *vm.VM, l *loop.Loop) error {
if v, err := vm.Get("Promise"); err != nil {
return err
} else if !v.IsUndefined() {

View File

@ -0,0 +1,101 @@
package promise_test
import (
"testing"
"time"
"github.com/robertkrimen/otto"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/promise"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
func (s *PromiseSuite) TestResolve() {
err := s.vm.Set("__resolve", func(str string) {
defer func() { s.ch <- struct{}{} }()
s.Equal("good", str)
})
s.NoError(err)
err = s.loop.Eval(`
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('good');
}, 10);
});
p.then(function(d) {
__resolve(d);
});
p.catch(function(err) {
throw err;
});
`)
s.NoError(err)
select {
case <-s.ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
return
}
}
func (s *PromiseSuite) TestReject() {
err := s.vm.Set("__reject", func(str string) {
defer func() { s.ch <- struct{}{} }()
s.Equal("bad", str)
})
s.NoError(err)
err = s.loop.Eval(`
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
reject('bad');
}, 10);
});
p.catch(function(err) {
__reject(err);
});
`)
s.NoError(err)
select {
case <-s.ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
return
}
}
type PromiseSuite struct {
suite.Suite
loop *loop.Loop
vm *vm.VM
ch chan struct{}
}
func (s *PromiseSuite) SetupTest() {
o := otto.New()
s.vm = vm.New(o)
s.loop = loop.New(s.vm)
go s.loop.Run()
err := promise.Define(s.vm, s.loop)
s.NoError(err)
s.ch = make(chan struct{})
}
func TestPromiseSuite(t *testing.T) {
suite.Run(t, new(PromiseSuite))
}

View File

@ -5,7 +5,8 @@ import (
"github.com/robertkrimen/otto"
"github.com/status-im/ottoext/loop"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
var minDelay = map[bool]int64{
@ -13,7 +14,7 @@ var minDelay = map[bool]int64{
false: 4,
}
func Define(vm *otto.Otto, l *loop.Loop) error {
func Define(vm *vm.VM, l *loop.Loop) error {
if v, err := vm.Get("setTimeout"); err != nil {
return err
} else if !v.IsUndefined() {
@ -97,7 +98,7 @@ type timerTask struct {
func (t *timerTask) SetID(id int64) { t.id = id }
func (t *timerTask) GetID() int64 { return t.id }
func (t *timerTask) Execute(vm *otto.Otto, l *loop.Loop) error {
func (t *timerTask) Execute(vm *vm.VM, l *loop.Loop) error {
var arguments []interface{}
if len(t.call.ArgumentList) > 2 {

View File

@ -0,0 +1,119 @@
package timers_test
import (
"testing"
"time"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/timers"
"github.com/status-im/status-go/geth/jail/internal/vm"
"github.com/stretchr/testify/suite"
)
func (s *TimersSuite) TestSetTimeout() {
err := s.vm.Set("__capture", func() {
s.ch <- struct{}{}
})
s.NoError(err)
err = s.loop.Eval(`setTimeout(function(n) {
if (Date.now() - n < 50) {
throw new Error('timeout was called too soon');
}
__capture();
}, 50, Date.now());`)
s.NoError(err)
select {
case <-s.ch:
case <-time.After(1 * time.Second):
s.Fail("test timed out")
return
}
}
func (s *TimersSuite) TestClearTimeout() {
err := s.vm.Set("__shouldNeverRun", func() {
s.Fail("should never run")
})
s.NoError(err)
err = s.loop.Eval(`clearTimeout(setTimeout(function() {
__shouldNeverRun();
}, 50));`)
s.NoError(err)
<-time.After(100 * time.Millisecond)
}
func (s *TimersSuite) TestSetInterval() {
err := s.vm.Set("__done", func() {
s.ch <- struct{}{}
})
s.NoError(err)
err = s.loop.Eval(`
var c = 0;
var iv = setInterval(function() {
if (c === 1) {
clearInterval(iv);
__done();
}
c++;
}, 50);
`)
s.NoError(err)
select {
case <-s.ch:
value, err := s.vm.Get("c")
s.NoError(err)
n, err := value.ToInteger()
s.NoError(err)
s.Equal(2, int(n))
case <-time.After(1 * time.Second):
s.Fail("test timed out")
}
}
func (s *TimersSuite) TestClearIntervalImmediately() {
err := s.vm.Set("__shouldNeverRun", func() {
s.Fail("should never run")
})
s.NoError(err)
err = s.loop.Eval(`clearInterval(setInterval(function() {
__shouldNeverRun();
}, 50));`)
s.NoError(err)
<-time.After(100 * time.Millisecond)
}
type TimersSuite struct {
suite.Suite
loop *loop.Loop
vm *vm.VM
ch chan struct{}
}
func (s *TimersSuite) SetupTest() {
o := otto.New()
s.vm = vm.New(o)
s.loop = loop.New(s.vm)
go s.loop.Run()
err := timers.Define(s.vm, s.loop)
s.NoError(err)
s.ch = make(chan struct{})
}
func TestTimersSuite(t *testing.T) {
suite.Run(t, new(TimersSuite))
}

View File

@ -0,0 +1,71 @@
package vm
import (
"sync"
"github.com/robertkrimen/otto"
)
// VM implements concurrency safe wrapper to
// otto's VM object.
type VM struct {
sync.Mutex
vm *otto.Otto
}
// New creates new instance of VM.
func New(vm *otto.Otto) *VM {
return &VM{
vm: vm,
}
}
// Set sets the value to be keyed by the provided keyname.
func (vm *VM) Set(key string, val interface{}) error {
vm.Lock()
defer vm.Unlock()
return vm.vm.Set(key, val)
}
// Get returns the giving key's otto.Value from the underline otto vm.
func (vm *VM) Get(key string) (otto.Value, error) {
vm.Lock()
defer vm.Unlock()
return vm.vm.Get(key)
}
// Call attempts to call the internal call function for the giving response associated with the
// proper values.
func (vm *VM) Call(item string, this interface{}, args ...interface{}) (otto.Value, error) {
vm.Lock()
defer vm.Unlock()
return vm.vm.Call(item, this, args...)
}
// Run evaluates JS source, which may be string or otto.Script variable.
func (vm *VM) Run(src interface{}) (otto.Value, error) {
vm.Lock()
defer vm.Unlock()
return vm.vm.Run(src)
}
// Compile parses given source and returns otto.Script.
func (vm *VM) Compile(filename string, src interface{}) (*otto.Script, error) {
vm.Lock()
defer vm.Unlock()
return vm.vm.Compile(filename, src)
}
// CompileWithSourceMap parses given source with source map and returns otto.Script.
func (vm *VM) CompileWithSourceMap(filename string, src, sm interface{}) (*otto.Script, error) {
vm.Lock()
defer vm.Unlock()
return vm.vm.CompileWithSourceMap(filename, src, sm)
}

View File

@ -1,74 +1,50 @@
package jail
import (
"sync"
"github.com/robertkrimen/otto"
"github.com/status-im/ottoext/loop"
"github.com/status-im/ottoext/timers"
"github.com/status-im/status-go/geth/jail/internal/fetch"
"github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/status-go/geth/jail/internal/timers"
"github.com/status-im/status-go/geth/jail/internal/vm"
)
// Cell represents a single jail cell, which is basically a JavaScript VM.
type Cell struct {
sync.Mutex
id string
vm *otto.Otto
*vm.VM
}
// newCell encapsulates what we need to create a new jailCell from the
// provided vm and eventloop instance.
func newCell(id string, vm *otto.Otto) (*Cell, error) {
// create new event loop for the new cell.
// this loop is handling 'setTimeout/setInterval'
// calls and is running endlessly in a separate goroutine
lo := loop.New(vm)
func newCell(id string, ottoVM *otto.Otto) (*Cell, error) {
cellVM := vm.New(ottoVM)
// register handlers for setTimeout/setInterval
// functions
if err := timers.Define(vm, lo); err != nil {
return nil, err
}
lo := loop.New(cellVM)
// finally, start loop in a goroutine
registerVMHandlers(cellVM, lo)
// start loop in a goroutine
// Cell is currently immortal, so the loop
go lo.Run()
return &Cell{
id: id,
vm: vm,
VM: cellVM,
}, nil
}
// Set sets the value to be keyed by the provided keyname.
func (cell *Cell) Set(key string, val interface{}) error {
cell.Lock()
defer cell.Unlock()
// registerHandlers register variuous functions and handlers
// to the Otto VM, such as Fetch API callbacks or promises.
func registerVMHandlers(v *vm.VM, lo *loop.Loop) error {
// setTimeout/setInterval functions
if err := timers.Define(v, lo); err != nil {
return err
}
return cell.vm.Set(key, val)
}
// Get returns the giving key's otto.Value from the underline otto vm.
func (cell *Cell) Get(key string) (otto.Value, error) {
cell.Lock()
defer cell.Unlock()
return cell.vm.Get(key)
}
// Call attempts to call the internal call function for the giving response associated with the
// proper values.
func (cell *Cell) Call(item string, this interface{}, args ...interface{}) (otto.Value, error) {
cell.Lock()
defer cell.Unlock()
return cell.vm.Call(item, this, args...)
}
// Run evaluates the giving js string on the associated vm llop.
func (cell *Cell) Run(val string) (otto.Value, error) {
cell.Lock()
defer cell.Unlock()
return cell.vm.Run(val)
// FetchAPI functions
if err := fetch.Define(v, lo); err != nil {
return err
}
return nil
}

View File

@ -1,10 +1,11 @@
package jail_test
import (
"net/http"
"net/http/httptest"
"time"
"github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/params"
)
func (s *JailTestSuite) TestJailTimeoutFailure() {
@ -70,9 +71,6 @@ func (s *JailTestSuite) TestJailTimeout() {
func (s *JailTestSuite) TestJailLoopInCall() {
require := s.Require()
s.StartTestNode(params.RopstenNetworkID)
defer s.StopTestNode()
// load Status JS and add test command to it
s.jail.BaseJS(baseStatusJSCode)
s.jail.Parse(testChatID, ``)
@ -109,3 +107,188 @@ func (s *JailTestSuite) TestJailLoopInCall() {
require.Fail("Failed to received event response")
}
}
// TestJailLoopRace tests multiple setTimeout callbacks,
// supposed to be run with '-race' flag.
func (s *JailTestSuite) TestJailLoopRace() {
require := s.Require()
cell, err := s.jail.NewCell(testChatID)
require.NoError(err)
require.NotNil(cell)
items := make(chan struct{})
err = cell.Set("__captureResponse", func() otto.Value {
go func() { items <- struct{}{} }()
return otto.UndefinedValue()
})
require.NoError(err)
_, err = cell.Run(`
function callRunner(){
return setTimeout(function(){
__captureResponse();
}, 1000);
}
`)
require.NoError(err)
for i := 0; i < 100; i++ {
_, err = cell.Call("callRunner", nil)
require.NoError(err)
}
for i := 0; i < 100; i++ {
select {
case <-items:
case <-time.After(5 * time.Second):
require.Fail("test timed out")
}
}
}
func (s *JailTestSuite) TestJailFetchPromise() {
body := `{"key": "value"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(body))
}))
defer server.Close()
require := s.Require()
cell, err := s.jail.NewCell(testChatID)
require.NoError(err)
require.NotNil(cell)
dataCh := make(chan otto.Value, 1)
errCh := make(chan otto.Value, 1)
err = cell.Set("__captureSuccess", func(res otto.Value) { dataCh <- res })
require.NoError(err)
err = cell.Set("__captureError", func(res otto.Value) { errCh <- res })
require.NoError(err)
// run JS code for fetching valid URL
_, err = cell.Run(`fetch('` + server.URL + `').then(function(r) {
return r.text()
}).then(function(data) {
__captureSuccess(data)
}).catch(function (e) {
__captureError(e)
})`)
require.NoError(err)
select {
case data := <-dataCh:
require.True(data.IsString())
require.Equal(body, data.String())
case err := <-errCh:
require.Fail("request failed", err)
case <-time.After(1 * time.Second):
require.Fail("test timed out")
}
}
func (s *JailTestSuite) TestJailFetchCatch() {
require := s.Require()
cell, err := s.jail.NewCell(testChatID)
require.NoError(err)
require.NotNil(cell)
dataCh := make(chan otto.Value, 1)
errCh := make(chan otto.Value, 1)
err = cell.Set("__captureSuccess", func(res otto.Value) { dataCh <- res })
require.NoError(err)
err = cell.Set("__captureError", func(res otto.Value) { errCh <- res })
require.NoError(err)
// run JS code for fetching invalid URL
_, err = cell.Run(`fetch('http://👽/nonexistent').then(function(r) {
return r.text()
}).then(function(data) {
__captureSuccess(data)
}).catch(function (e) {
__captureError(e)
})`)
require.NoError(err)
select {
case data := <-dataCh:
require.Fail("request should have failed, but returned", data)
case e := <-errCh:
require.True(e.IsObject())
name, err := e.Object().Get("name")
require.NoError(err)
require.Equal("Error", name.String())
_, err = e.Object().Get("message")
require.NoError(err)
case <-time.After(1 * time.Second):
require.Fail("test timed out")
}
}
// TestJailFetchRace tests multiple fetch callbacks,
// supposed to be run with '-race' flag.
func (s *JailTestSuite) TestJailFetchRace() {
body := `{"key": "value"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(body))
}))
defer server.Close()
require := s.Require()
cell, err := s.jail.NewCell(testChatID)
require.NoError(err)
require.NotNil(cell)
dataCh := make(chan otto.Value, 1)
errCh := make(chan otto.Value, 1)
err = cell.Set("__captureSuccess", func(res otto.Value) { dataCh <- res })
require.NoError(err)
err = cell.Set("__captureError", func(res otto.Value) { errCh <- res })
require.NoError(err)
// run JS code for fetching valid URL
_, err = cell.Run(`fetch('` + server.URL + `').then(function(r) {
return r.text()
}).then(function(data) {
__captureSuccess(data)
}).catch(function (e) {
__captureError(e)
})`)
require.NoError(err)
// run JS code for fetching invalid URL
_, err = cell.Run(`fetch('http://👽/nonexistent').then(function(r) {
return r.text()
}).then(function(data) {
__captureSuccess(data)
}).catch(function (e) {
__captureError(e)
})`)
require.NoError(err)
for i := 0; i < 2; i++ {
select {
case data := <-dataCh:
require.True(data.IsString())
require.Equal(body, data.String())
case e := <-errCh:
require.True(e.IsObject())
name, err := e.Object().Get("name")
require.NoError(err)
require.Equal("Error", name.String())
_, err = e.Object().Get("message")
require.NoError(err)
case <-time.After(1 * time.Second):
require.Fail("test timed out")
return
}
}
}

View File

@ -1 +0,0 @@
/ottoext

View File

@ -1,12 +0,0 @@
var x = fetch('http://www.example.com/').then(function(r) {
r.text().then(function(d) {
console.log(r.statusText);
for (var k in r.headers._headers) {
console.log(k + ':', r.headers.get(k));
}
console.log('');
console.log(d);
});
});

View File

@ -1,90 +0,0 @@
// command otto runs JavaScript from a file, opens a repl, or does both.
package main
import (
"flag"
"io"
"io/ioutil"
"github.com/status-im/ottoext/loop"
"github.com/status-im/ottoext/loop/looptask"
erepl "github.com/status-im/ottoext/repl"
"github.com/robertkrimen/otto"
"github.com/robertkrimen/otto/repl"
"github.com/status-im/ottoext/fetch"
"github.com/status-im/ottoext/process"
"github.com/status-im/ottoext/promise"
"github.com/status-im/ottoext/timers"
)
var (
openRepl = flag.Bool("repl", false, "Always open a REPL, even if a file is provided.")
debugger = flag.Bool("debugger", false, "Attach REPL-based debugger.")
)
func main() {
flag.Parse()
vm := otto.New()
if *debugger {
vm.SetDebuggerHandler(repl.DebuggerHandler)
}
l := loop.New(vm)
if err := timers.Define(vm, l); err != nil {
panic(err)
}
if err := promise.Define(vm, l); err != nil {
panic(err)
}
if err := fetch.Define(vm, l); err != nil {
panic(err)
}
if err := process.Define(vm, flag.Args()); err != nil {
panic(err)
}
blockingTask := looptask.NewEvalTask("")
if len(flag.Args()) == 0 || *openRepl {
l.Add(blockingTask)
}
if len(flag.Args()) > 0 {
d, err := ioutil.ReadFile(flag.Arg(0))
if err != nil {
panic(err)
}
// this is a very cheap way of "supporting" shebang lines
if d[0] == '#' {
d = []byte("// " + string(d))
}
s, err := vm.Compile(flag.Arg(0), string(d))
if err != nil {
panic(err)
}
if err := l.Eval(s); err != nil {
panic(err)
}
}
if len(flag.Args()) == 0 || *openRepl {
go func() {
if err := erepl.Run(l); err != nil && err != io.EOF {
panic(err)
}
l.Ready(blockingTask)
}()
}
if err := l.Run(); err != nil {
panic(err)
}
}

View File

@ -1,236 +0,0 @@
package repl
import (
"fmt"
"strings"
"github.com/robertkrimen/otto"
)
func seenWith(seen map[otto.Value]bool, v otto.Value) map[otto.Value]bool {
r := make(map[otto.Value]bool)
for k, v := range seen {
r[k] = v
}
r[v] = true
return r
}
func format(v otto.Value, width, indent, limit int) (string, error) {
return formatIndent(v, width, indent, limit, 0, 0, make(map[otto.Value]bool))
}
func formatIndent(v otto.Value, width, indent, limit, level, additional int, seen map[otto.Value]bool) (string, error) {
if limit == 0 {
return "...", nil
}
switch {
case v.IsBoolean(), v.IsNull(), v.IsNumber(), v.IsUndefined():
return v.String(), nil
case v.IsString():
return fmt.Sprintf("%q", v.String()), nil
case v.IsFunction():
n, err := v.Object().Get("name")
if err != nil {
return "", err
}
if n.IsUndefined() {
return "function", nil
}
return fmt.Sprintf("function %s", n.String()), nil
case v.IsObject():
if d, err := formatOneLine(v, limit, seen); err != nil {
return "", err
} else if level*indent+additional+len(d) <= width {
return d, nil
}
switch v.Class() {
case "Array":
return formatArray(v, width, indent, limit, level, seen)
default:
return formatObject(v, width, indent, limit, level, seen)
}
default:
return "", fmt.Errorf("couldn't format type %s", v.Class())
}
}
func formatArray(v otto.Value, width, indent, limit, level int, seen map[otto.Value]bool) (string, error) {
if seen[v] {
return strings.Repeat(" ", level*indent) + "[circular]", nil
}
o := v.Object()
lv, err := o.Get("length")
if err != nil {
return "", err
}
li, err := lv.Export()
if err != nil {
return "", err
}
l, ok := li.(uint32)
if !ok {
return "", fmt.Errorf("length property must be a number; was %T", li)
}
bits := []string{"["}
for i := 0; i < int(l); i++ {
e, err := o.Get(fmt.Sprintf("%d", i))
if err != nil {
return "", err
}
d, err := formatIndent(e, width, indent, limit-1, level+1, 0, seenWith(seen, v))
if err != nil {
return "", err
}
bits = append(bits, strings.Repeat(" ", (level+1)*indent)+d+",")
}
bits = append(bits, strings.Repeat(" ", level*indent)+"]")
return strings.Join(bits, "\n"), nil
}
func formatObject(v otto.Value, width, indent, limit, level int, seen map[otto.Value]bool) (string, error) {
if seen[v] {
return strings.Repeat(" ", level*indent) + "[circular]", nil
}
o := v.Object()
bits := []string{"{"}
keys := o.Keys()
for i, k := range keys {
e, err := o.Get(k)
d, err := formatIndent(e, width, indent, limit-1, level+1, len(k)+2, seenWith(seen, v))
if err != nil {
return "", err
}
bits = append(bits, strings.Repeat(" ", (level+1)*indent)+k+": "+d+",")
i++
}
bits = append(bits, strings.Repeat(" ", level*indent)+"}")
return strings.Join(bits, "\n"), nil
}
func formatOneLine(v otto.Value, limit int, seen map[otto.Value]bool) (string, error) {
if limit == 0 {
return "...", nil
}
switch {
case v.IsBoolean(), v.IsNull(), v.IsNumber(), v.IsUndefined():
return v.String(), nil
case v.IsString():
return fmt.Sprintf("%q", v.String()), nil
case v.IsFunction():
n, err := v.Object().Get("name")
if err != nil {
return "", err
}
if n.IsUndefined() {
return "function", nil
}
return fmt.Sprintf("function %s", n.String()), nil
case v.IsObject():
switch v.Class() {
case "Array":
return formatArrayOneLine(v, limit, seen)
default:
return formatObjectOneLine(v, limit, seen)
}
default:
return "", fmt.Errorf("couldn't format type %s", v.Class())
}
}
func formatArrayOneLine(v otto.Value, limit int, seen map[otto.Value]bool) (string, error) {
if limit == 0 {
return "...", nil
}
if seen[v] {
return "[circular]", nil
}
o := v.Object()
lv, err := o.Get("length")
if err != nil {
return "", err
}
li, err := lv.Export()
if err != nil {
return "", err
}
l, ok := li.(uint32)
if !ok {
return "", fmt.Errorf("length property must be a number; was %T", li)
}
var bits []string
for i := 0; i < int(l); i++ {
e, err := o.Get(fmt.Sprintf("%d", i))
if err != nil {
return "", err
}
d, err := formatOneLine(e, limit-1, seenWith(seen, v))
if err != nil {
return "", err
}
bits = append(bits, d)
}
return "[" + strings.Join(bits, ", ") + "]", nil
}
func formatObjectOneLine(v otto.Value, limit int, seen map[otto.Value]bool) (string, error) {
if limit == 0 {
return "...", nil
}
if seen[v] {
return "[circular]", nil
}
o := v.Object()
bits := []string{}
keys := o.Keys()
for _, k := range keys {
e, err := o.Get(k)
if err != nil {
return "", err
}
d, err := formatOneLine(e, limit-1, seenWith(seen, v))
if err != nil {
return "", err
}
bits = append(bits, k+": "+d)
}
return "{" + strings.Join(bits, ", ") + "}", nil
}

View File

@ -1,151 +0,0 @@
// Package repl implements an event loop aware REPL (read-eval-print loop)
// for otto.
package repl
import (
"fmt"
"io"
"strings"
"github.com/robertkrimen/otto"
"github.com/robertkrimen/otto/parser"
"github.com/status-im/ottoext/loop"
"github.com/status-im/ottoext/loop/looptask"
"gopkg.in/readline.v1"
)
// Run creates a REPL with the default prompt and no prelude.
func Run(l *loop.Loop) error {
return RunWithPromptAndPrelude(l, "", "")
}
// RunWithPrompt runs a REPL with the given prompt and no prelude.
func RunWithPrompt(l *loop.Loop, prompt string) error {
return RunWithPromptAndPrelude(l, prompt, "")
}
// RunWithPrelude runs a REPL with the default prompt and the given prelude.
func RunWithPrelude(l *loop.Loop, prelude string) error {
return RunWithPromptAndPrelude(l, "", prelude)
}
// RunWithPromptAndPrelude runs a REPL with the given prompt and prelude.
func RunWithPromptAndPrelude(l *loop.Loop, prompt, prelude string) error {
if prompt == "" {
prompt = ">"
}
prompt = strings.Trim(prompt, " ")
prompt += " "
rl, err := readline.New(prompt)
if err != nil {
return err
}
l.VM().Set("console", map[string]interface{}{
"log": func(c otto.FunctionCall) otto.Value {
s := make([]string, len(c.ArgumentList))
for i := 0; i < len(c.ArgumentList); i++ {
s[i] = c.Argument(i).String()
}
rl.Stdout().Write([]byte(strings.Join(s, " ") + "\n"))
rl.Refresh()
return otto.UndefinedValue()
},
"warn": func(c otto.FunctionCall) otto.Value {
s := make([]string, len(c.ArgumentList))
for i := 0; i < len(c.ArgumentList); i++ {
s[i] = c.Argument(i).String()
}
rl.Stderr().Write([]byte(strings.Join(s, " ") + "\n"))
rl.Refresh()
return otto.UndefinedValue()
},
})
if prelude != "" {
if _, err := io.Copy(rl.Stderr(), strings.NewReader(prelude+"\n")); err != nil {
return err
}
rl.Refresh()
}
var d []string
for {
ll, err := rl.Readline()
if err != nil {
if err == readline.ErrInterrupt {
if d != nil {
d = nil
rl.SetPrompt(prompt)
rl.Refresh()
continue
}
break
}
return err
}
if len(d) == 0 && ll == "" {
continue
}
d = append(d, ll)
s := strings.Join(d, "\n")
if _, err := parser.ParseFile(nil, "repl", s, 0); err != nil {
rl.SetPrompt(strings.Repeat(" ", len(prompt)))
} else {
rl.SetPrompt(prompt)
d = nil
t := looptask.NewEvalTask(s)
// don't report errors to the loop - this lets us handle them and
// resume normal operation
t.SoftError = true
l.Add(t)
l.Ready(t)
v, err := <-t.Value, <-t.Error
if err != nil {
if oerr, ok := err.(*otto.Error); ok {
io.Copy(rl.Stdout(), strings.NewReader(oerr.String()))
} else {
io.Copy(rl.Stdout(), strings.NewReader(err.Error()))
}
} else {
f, err := format(v, 80, 2, 5)
if err != nil {
panic(err)
}
rl.Stdout().Write([]byte("\r" + f + "\n"))
}
}
rl.Refresh()
}
return rl.Close()
}
func inspect(v otto.Value, width, indent int) string {
switch {
case v.IsBoolean(), v.IsNull(), v.IsNumber(), v.IsString(), v.IsUndefined(), v.IsNaN():
return fmt.Sprintf("%s%q", strings.Repeat(" ", indent), v.String())
default:
return ""
}
}