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. // 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 { type JailCell interface {
// Set a value inside VM.
Set(string, interface{}) error Set(string, interface{}) error
// Get a value from VM.
Get(string) (otto.Value, error) 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) 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" 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 Package jail implements "jailed" enviroment for executing arbitrary JavaScript
code using Otto JS interpreter (https://github.com/robertkrimen/otto). code using Otto JS interpreter (https://github.com/robertkrimen/otto).
Jail create multiple JailCells, one cell per status client chat. Each cell runs Jail create multiple Cells, one cell per status client chat. Each cell runs own
own Otto virtual machine and lives forever, but that may change in the future. Otto virtual machine and lives forever, but that may change in the future.
+----------------------------------------------+ +----------------------------------------------+
| Jail | | 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 Each Cell object embeds *VM from 'jail/vm' for concurrency safe wrapper around
for Otto VM Get and Set functions respectively. See Otto documentation for usage *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 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. (*VM).Call/Run functions allows executing arbitrary JS in the cell. They're also
They're also wrappers arount Otto VM functions of the same name. Run accepts raw wrappers arount Otto VM functions of the same name. Run accepts raw JS strings
JS strings for execution, Call takes a JS function name (defined in VM) and for execution, Call takes a JS function name (defined in VM) and parameters.
parameters.
# Timeouts and intervals support ### Timeouts and intervals support
Default Otto VM interpreter doesn't support setTimeout()/setInterval() JS Default Otto VM interpreter doesn't support setTimeout()/setInterval() JS
functions, because they're not part of ECMA-262 spec, but properties of the functions, because they're not part of ECMA-262 spec, but properties of the
window object in browser. We add support for them using window object in browser. We add support for them using
http://github.com/deoxxa/ottoext/timers and http://github.com/status-im/ottoext/timers and
http://github.com/deoxxa/ottoext/loop packages. http://github.com/status-im/ottoext/loop packages.
Each cell starts a new loop in a separate goroutine, registers functions for 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 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);`) 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 Package jail implements "jailed" enviroment for executing arbitrary
JavaScript code using Otto JS interpreter (https://github.com/robertkrimen/otto). 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. 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 || || 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 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: Otto VM Get and Set functions respectively. See Otto documentation for usage examples:
https://godoc.org/github.com/robertkrimen/otto 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 (*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, 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. 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, 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. 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. packages.
Each cell starts a new loop in a separate goroutine, registers functions for setTimeout/setInterval 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 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 package jail

View File

@ -1,7 +1,9 @@
ottoext 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 Overview
-------- --------

View File

@ -10,8 +10,9 @@ import (
"github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
"github.com/status-im/ottoext/loop" "github.com/status-im/status-go/geth/jail/internal/loop"
"github.com/status-im/ottoext/promise" "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 { 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) SetID(id int64) { t.id = id }
func (t *fetchTask) GetID() int64 { return t.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{} var arguments []interface{}
if t.err != nil { if t.err != nil {
@ -48,6 +49,12 @@ func (t *fetchTask) Execute(vm *otto.Otto, l *loop.Loop) error {
arguments = append(arguments, e) 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("status", t.status)
t.jsRes.Set("statusText", t.statusText) t.jsRes.Set("statusText", t.statusText)
h := mustValue(t.jsRes.Get("headers")).Object() 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 (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) 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 { if err := promise.Define(vm, l); err != nil {
return err 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"
"sync/atomic" "sync/atomic"
"github.com/robertkrimen/otto" "github.com/status-im/status-go/geth/jail/internal/vm"
) )
func formatTask(t Task) string { func formatTask(t Task) string {
@ -29,7 +29,7 @@ func formatTask(t Task) string {
type Task interface { type Task interface {
SetID(id int64) SetID(id int64)
GetID() int64 GetID() int64
Execute(vm *otto.Otto, l *Loop) error Execute(vm *vm.VM, l *Loop) error
Cancel() Cancel()
} }
@ -39,7 +39,7 @@ type Task interface {
// to finalise on the VM. The channel holding the tasks pending finalising can // to finalise on the VM. The channel holding the tasks pending finalising can
// be buffered or unbuffered. // be buffered or unbuffered.
type Loop struct { type Loop struct {
vm *otto.Otto vm *vm.VM
id int64 id int64
lock sync.RWMutex lock sync.RWMutex
tasks map[int64]Task 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. // 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) return NewWithBacklog(vm, 0)
} }
// NewWithBacklog creates a new Loop on a specific VM, giving it a buffered // NewWithBacklog creates a new Loop on a specific VM, giving it a buffered
// queue, the capacity of which being specified by the backlog argument. // 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{ return &Loop{
vm: vm, vm: vm,
tasks: make(map[int64]Task), 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 // VM gets the JavaScript interpreter associated with the loop.
// some kind of Otto object, but it's wrapped in an interface so the func (l *Loop) VM() *vm.VM {
// `ottoext` library can work with forks/extensions of otto.
func (l *Loop) VM() *otto.Otto {
return l.vm return l.vm
} }
@ -107,15 +105,6 @@ func (l *Loop) Ready(t Task) {
l.ready <- t 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 // Eval executes some code in the VM associated with the loop and returns an
// error if that execution fails. // error if that execution fails.
func (l *Loop) Eval(s interface{}) error { func (l *Loop) Eval(s interface{}) error {

View File

@ -3,8 +3,9 @@ package looptask
import ( import (
"errors" "errors"
"github.com/status-im/ottoext/loop"
"github.com/robertkrimen/otto" "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 // 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 // Execute always returns an error for an IdleTask, as it should never
// actually be run. // 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") 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 // Execute runs the EvalTask's otto.Script in the vm provided, pushing the
// resultant return value and error (or nil) into the associated channels. // resultant return value and error (or nil) into the associated channels.
// If the execution results in an error, it will return that error. // 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) v, err := vm.Run(e.Script)
e.Value <- v e.Value <- v
e.Error <- err e.Error <- err

View File

@ -1,13 +1,12 @@
package promise package promise
import ( import (
"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/ottoext/loop" "github.com/status-im/status-go/geth/jail/internal/vm"
"github.com/status-im/ottoext/timers"
) )
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 { if v, err := vm.Get("Promise"); err != nil {
return err return err
} else if !v.IsUndefined() { } 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/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{ var minDelay = map[bool]int64{
@ -13,7 +14,7 @@ var minDelay = map[bool]int64{
false: 4, 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 { if v, err := vm.Get("setTimeout"); err != nil {
return err return err
} else if !v.IsUndefined() { } else if !v.IsUndefined() {
@ -97,7 +98,7 @@ type timerTask struct {
func (t *timerTask) SetID(id int64) { t.id = id } func (t *timerTask) SetID(id int64) { t.id = id }
func (t *timerTask) GetID() int64 { return t.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{} var arguments []interface{}
if len(t.call.ArgumentList) > 2 { 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 package jail
import ( import (
"sync"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
"github.com/status-im/ottoext/loop" "github.com/status-im/status-go/geth/jail/internal/fetch"
"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"
) )
// Cell represents a single jail cell, which is basically a JavaScript VM. // Cell represents a single jail cell, which is basically a JavaScript VM.
type Cell struct { type Cell struct {
sync.Mutex
id string id string
vm *otto.Otto *vm.VM
} }
// newCell encapsulates what we need to create a new jailCell from the // newCell encapsulates what we need to create a new jailCell from the
// provided vm and eventloop instance. // provided vm and eventloop instance.
func newCell(id string, vm *otto.Otto) (*Cell, error) { func newCell(id string, ottoVM *otto.Otto) (*Cell, error) {
// create new event loop for the new cell. cellVM := vm.New(ottoVM)
// this loop is handling 'setTimeout/setInterval'
// calls and is running endlessly in a separate goroutine
lo := loop.New(vm)
// register handlers for setTimeout/setInterval lo := loop.New(cellVM)
// functions
if err := timers.Define(vm, lo); err != nil {
return nil, err
}
// finally, start loop in a goroutine registerVMHandlers(cellVM, lo)
// start loop in a goroutine
// Cell is currently immortal, so the loop // Cell is currently immortal, so the loop
go lo.Run() go lo.Run()
return &Cell{ return &Cell{
id: id, id: id,
vm: vm, VM: cellVM,
}, nil }, nil
} }
// Set sets the value to be keyed by the provided keyname. // registerHandlers register variuous functions and handlers
func (cell *Cell) Set(key string, val interface{}) error { // to the Otto VM, such as Fetch API callbacks or promises.
cell.Lock() func registerVMHandlers(v *vm.VM, lo *loop.Loop) error {
defer cell.Unlock() // setTimeout/setInterval functions
if err := timers.Define(v, lo); err != nil {
return err
}
return cell.vm.Set(key, val) // FetchAPI functions
} if err := fetch.Define(v, lo); err != nil {
return err
// Get returns the giving key's otto.Value from the underline otto vm. }
func (cell *Cell) Get(key string) (otto.Value, error) {
cell.Lock() return nil
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)
} }

View File

@ -1,10 +1,11 @@
package jail_test package jail_test
import ( import (
"net/http"
"net/http/httptest"
"time" "time"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
"github.com/status-im/status-go/geth/params"
) )
func (s *JailTestSuite) TestJailTimeoutFailure() { func (s *JailTestSuite) TestJailTimeoutFailure() {
@ -70,9 +71,6 @@ func (s *JailTestSuite) TestJailTimeout() {
func (s *JailTestSuite) TestJailLoopInCall() { func (s *JailTestSuite) TestJailLoopInCall() {
require := s.Require() require := s.Require()
s.StartTestNode(params.RopstenNetworkID)
defer s.StopTestNode()
// load Status JS and add test command to it // load Status JS and add test command to it
s.jail.BaseJS(baseStatusJSCode) s.jail.BaseJS(baseStatusJSCode)
s.jail.Parse(testChatID, ``) s.jail.Parse(testChatID, ``)
@ -109,3 +107,188 @@ func (s *JailTestSuite) TestJailLoopInCall() {
require.Fail("Failed to received event response") 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 ""
}
}