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:
parent
39aeb3b09d
commit
6a096607cf
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
--------
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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 {
|
|
@ -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
|
|
@ -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() {
|
|
@ -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))
|
||||
}
|
|
@ -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 {
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/ottoext
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 ""
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue