From 6a096607cf9133d0485a6bc8246af2f668e8eecc Mon Sep 17 00:00:00 2001 From: Ivan Daniluk Date: Fri, 8 Sep 2017 13:55:17 +0200 Subject: [PATCH] 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. --- geth/common/types.go | 7 +- geth/jail/README.md | 62 +++-- geth/jail/doc.go | 34 ++- .../ottoext => geth/jail/internal}/LICENSE.md | 0 .../ottoext => geth/jail/internal}/README.md | 4 +- .../jail/internal}/fetch/Makefile | 0 .../internal}/fetch/dist-fetch.rice-box.go | 0 .../jail/internal}/fetch/dist-fetch/bundle.js | 0 .../internal}/fetch/dist-fetch/bundle.js.map | 0 .../jail/internal}/fetch/fetch.go | 17 +- geth/jail/internal/fetch/fetch_test.go | 222 ++++++++++++++++ .../jail/internal}/fetch/js/.gitignore | 0 .../jail/internal}/fetch/js/bundle.js | 0 .../jail/internal}/fetch/js/bundle.js.map | 0 .../jail/internal}/fetch/js/fetch.js | 0 .../jail/internal}/fetch/js/headers.js | 0 .../jail/internal}/fetch/js/index.js | 0 .../jail/internal}/fetch/js/package.json | 0 .../jail/internal}/fetch/js/request.js | 0 .../jail/internal}/fetch/js/response.js | 0 .../jail/internal}/fetch/js/webpack.config.js | 0 .../jail/internal}/loop/loop.go | 25 +- .../jail/internal}/loop/looptask/tasks.go | 7 +- .../ottoext => geth/jail/internal}/main.go | 0 .../jail/internal}/process/process.go | 0 .../jail/internal}/promise/Makefile | 0 .../internal}/promise/dist-promise/bundle.js | 0 .../jail/internal}/promise/js.go | 0 .../jail/internal}/promise/promise.go | 9 +- geth/jail/internal/promise/promise_test.go | 101 ++++++++ .../jail/internal}/timers/timers.go | 7 +- geth/jail/internal/timers/timers_test.go | 119 +++++++++ geth/jail/internal/vm/vm.go | 71 ++++++ geth/jail/jail_cell.go | 74 ++---- geth/jail/jail_cell_test.go | 191 +++++++++++++- .../status-im/ottoext/cmd/ottoext/.gitignore | 1 - .../status-im/ottoext/cmd/ottoext/example.js | 12 - .../status-im/ottoext/cmd/ottoext/main.go | 90 ------- .../status-im/ottoext/repl/print.go | 236 ------------------ .../github.com/status-im/ottoext/repl/repl.go | 151 ----------- 40 files changed, 838 insertions(+), 602 deletions(-) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/LICENSE.md (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/README.md (66%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/Makefile (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/dist-fetch.rice-box.go (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/dist-fetch/bundle.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/dist-fetch/bundle.js.map (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/fetch.go (83%) create mode 100644 geth/jail/internal/fetch/fetch_test.go rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/.gitignore (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/bundle.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/bundle.js.map (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/fetch.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/headers.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/index.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/package.json (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/request.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/response.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/fetch/js/webpack.config.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/loop/loop.go (84%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/loop/looptask/tasks.go (93%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/main.go (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/process/process.go (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/promise/Makefile (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/promise/dist-promise/bundle.js (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/promise/js.go (100%) rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/promise/promise.go (61%) create mode 100644 geth/jail/internal/promise/promise_test.go rename {vendor/github.com/status-im/ottoext => geth/jail/internal}/timers/timers.go (91%) create mode 100644 geth/jail/internal/timers/timers_test.go create mode 100644 geth/jail/internal/vm/vm.go delete mode 100644 vendor/github.com/status-im/ottoext/cmd/ottoext/.gitignore delete mode 100644 vendor/github.com/status-im/ottoext/cmd/ottoext/example.js delete mode 100644 vendor/github.com/status-im/ottoext/cmd/ottoext/main.go delete mode 100644 vendor/github.com/status-im/ottoext/repl/print.go delete mode 100644 vendor/github.com/status-im/ottoext/repl/repl.go diff --git a/geth/common/types.go b/geth/common/types.go index 4cc59543c..f53d8ccd9 100644 --- a/geth/common/types.go +++ b/geth/common/types.go @@ -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) } diff --git a/geth/jail/README.md b/geth/jail/README.md index fdab0e937..504271f1a 100644 --- a/geth/jail/README.md +++ b/geth/jail/README.md @@ -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) + })) diff --git a/geth/jail/doc.go b/geth/jail/doc.go index 486e527fd..b52af0966 100644 --- a/geth/jail/doc.go +++ b/geth/jail/doc.go @@ -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 diff --git a/vendor/github.com/status-im/ottoext/LICENSE.md b/geth/jail/internal/LICENSE.md similarity index 100% rename from vendor/github.com/status-im/ottoext/LICENSE.md rename to geth/jail/internal/LICENSE.md diff --git a/vendor/github.com/status-im/ottoext/README.md b/geth/jail/internal/README.md similarity index 66% rename from vendor/github.com/status-im/ottoext/README.md rename to geth/jail/internal/README.md index c99999f58..cf80382cc 100644 --- a/vendor/github.com/status-im/ottoext/README.md +++ b/geth/jail/internal/README.md @@ -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 -------- diff --git a/vendor/github.com/status-im/ottoext/fetch/Makefile b/geth/jail/internal/fetch/Makefile similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/Makefile rename to geth/jail/internal/fetch/Makefile diff --git a/vendor/github.com/status-im/ottoext/fetch/dist-fetch.rice-box.go b/geth/jail/internal/fetch/dist-fetch.rice-box.go similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/dist-fetch.rice-box.go rename to geth/jail/internal/fetch/dist-fetch.rice-box.go diff --git a/vendor/github.com/status-im/ottoext/fetch/dist-fetch/bundle.js b/geth/jail/internal/fetch/dist-fetch/bundle.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/dist-fetch/bundle.js rename to geth/jail/internal/fetch/dist-fetch/bundle.js diff --git a/vendor/github.com/status-im/ottoext/fetch/dist-fetch/bundle.js.map b/geth/jail/internal/fetch/dist-fetch/bundle.js.map similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/dist-fetch/bundle.js.map rename to geth/jail/internal/fetch/dist-fetch/bundle.js.map diff --git a/vendor/github.com/status-im/ottoext/fetch/fetch.go b/geth/jail/internal/fetch/fetch.go similarity index 83% rename from vendor/github.com/status-im/ottoext/fetch/fetch.go rename to geth/jail/internal/fetch/fetch.go index 433bbfd2b..3069b6222 100644 --- a/vendor/github.com/status-im/ottoext/fetch/fetch.go +++ b/geth/jail/internal/fetch/fetch.go @@ -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 } diff --git a/geth/jail/internal/fetch/fetch_test.go b/geth/jail/internal/fetch/fetch_test.go new file mode 100644 index 000000000..e482caa12 --- /dev/null +++ b/geth/jail/internal/fetch/fetch_test.go @@ -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)) +} diff --git a/vendor/github.com/status-im/ottoext/fetch/js/.gitignore b/geth/jail/internal/fetch/js/.gitignore similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/.gitignore rename to geth/jail/internal/fetch/js/.gitignore diff --git a/vendor/github.com/status-im/ottoext/fetch/js/bundle.js b/geth/jail/internal/fetch/js/bundle.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/bundle.js rename to geth/jail/internal/fetch/js/bundle.js diff --git a/vendor/github.com/status-im/ottoext/fetch/js/bundle.js.map b/geth/jail/internal/fetch/js/bundle.js.map similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/bundle.js.map rename to geth/jail/internal/fetch/js/bundle.js.map diff --git a/vendor/github.com/status-im/ottoext/fetch/js/fetch.js b/geth/jail/internal/fetch/js/fetch.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/fetch.js rename to geth/jail/internal/fetch/js/fetch.js diff --git a/vendor/github.com/status-im/ottoext/fetch/js/headers.js b/geth/jail/internal/fetch/js/headers.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/headers.js rename to geth/jail/internal/fetch/js/headers.js diff --git a/vendor/github.com/status-im/ottoext/fetch/js/index.js b/geth/jail/internal/fetch/js/index.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/index.js rename to geth/jail/internal/fetch/js/index.js diff --git a/vendor/github.com/status-im/ottoext/fetch/js/package.json b/geth/jail/internal/fetch/js/package.json similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/package.json rename to geth/jail/internal/fetch/js/package.json diff --git a/vendor/github.com/status-im/ottoext/fetch/js/request.js b/geth/jail/internal/fetch/js/request.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/request.js rename to geth/jail/internal/fetch/js/request.js diff --git a/vendor/github.com/status-im/ottoext/fetch/js/response.js b/geth/jail/internal/fetch/js/response.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/response.js rename to geth/jail/internal/fetch/js/response.js diff --git a/vendor/github.com/status-im/ottoext/fetch/js/webpack.config.js b/geth/jail/internal/fetch/js/webpack.config.js similarity index 100% rename from vendor/github.com/status-im/ottoext/fetch/js/webpack.config.js rename to geth/jail/internal/fetch/js/webpack.config.js diff --git a/vendor/github.com/status-im/ottoext/loop/loop.go b/geth/jail/internal/loop/loop.go similarity index 84% rename from vendor/github.com/status-im/ottoext/loop/loop.go rename to geth/jail/internal/loop/loop.go index 6f6c33cc8..415c523d3 100644 --- a/vendor/github.com/status-im/ottoext/loop/loop.go +++ b/geth/jail/internal/loop/loop.go @@ -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 { diff --git a/vendor/github.com/status-im/ottoext/loop/looptask/tasks.go b/geth/jail/internal/loop/looptask/tasks.go similarity index 93% rename from vendor/github.com/status-im/ottoext/loop/looptask/tasks.go rename to geth/jail/internal/loop/looptask/tasks.go index 74ae3d3ae..ba1a30cc8 100644 --- a/vendor/github.com/status-im/ottoext/loop/looptask/tasks.go +++ b/geth/jail/internal/loop/looptask/tasks.go @@ -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 diff --git a/vendor/github.com/status-im/ottoext/main.go b/geth/jail/internal/main.go similarity index 100% rename from vendor/github.com/status-im/ottoext/main.go rename to geth/jail/internal/main.go diff --git a/vendor/github.com/status-im/ottoext/process/process.go b/geth/jail/internal/process/process.go similarity index 100% rename from vendor/github.com/status-im/ottoext/process/process.go rename to geth/jail/internal/process/process.go diff --git a/vendor/github.com/status-im/ottoext/promise/Makefile b/geth/jail/internal/promise/Makefile similarity index 100% rename from vendor/github.com/status-im/ottoext/promise/Makefile rename to geth/jail/internal/promise/Makefile diff --git a/vendor/github.com/status-im/ottoext/promise/dist-promise/bundle.js b/geth/jail/internal/promise/dist-promise/bundle.js similarity index 100% rename from vendor/github.com/status-im/ottoext/promise/dist-promise/bundle.js rename to geth/jail/internal/promise/dist-promise/bundle.js diff --git a/vendor/github.com/status-im/ottoext/promise/js.go b/geth/jail/internal/promise/js.go similarity index 100% rename from vendor/github.com/status-im/ottoext/promise/js.go rename to geth/jail/internal/promise/js.go diff --git a/vendor/github.com/status-im/ottoext/promise/promise.go b/geth/jail/internal/promise/promise.go similarity index 61% rename from vendor/github.com/status-im/ottoext/promise/promise.go rename to geth/jail/internal/promise/promise.go index ba41c3f06..85c6e28a3 100644 --- a/vendor/github.com/status-im/ottoext/promise/promise.go +++ b/geth/jail/internal/promise/promise.go @@ -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() { diff --git a/geth/jail/internal/promise/promise_test.go b/geth/jail/internal/promise/promise_test.go new file mode 100644 index 000000000..618070fc6 --- /dev/null +++ b/geth/jail/internal/promise/promise_test.go @@ -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)) +} diff --git a/vendor/github.com/status-im/ottoext/timers/timers.go b/geth/jail/internal/timers/timers.go similarity index 91% rename from vendor/github.com/status-im/ottoext/timers/timers.go rename to geth/jail/internal/timers/timers.go index 315913cfe..70df71ce8 100644 --- a/vendor/github.com/status-im/ottoext/timers/timers.go +++ b/geth/jail/internal/timers/timers.go @@ -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 { diff --git a/geth/jail/internal/timers/timers_test.go b/geth/jail/internal/timers/timers_test.go new file mode 100644 index 000000000..15781ceca --- /dev/null +++ b/geth/jail/internal/timers/timers_test.go @@ -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)) +} diff --git a/geth/jail/internal/vm/vm.go b/geth/jail/internal/vm/vm.go new file mode 100644 index 000000000..e48e1e716 --- /dev/null +++ b/geth/jail/internal/vm/vm.go @@ -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) +} diff --git a/geth/jail/jail_cell.go b/geth/jail/jail_cell.go index 101be280a..57d407182 100644 --- a/geth/jail/jail_cell.go +++ b/geth/jail/jail_cell.go @@ -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 } diff --git a/geth/jail/jail_cell_test.go b/geth/jail/jail_cell_test.go index 2f29023bd..ac1003256 100644 --- a/geth/jail/jail_cell_test.go +++ b/geth/jail/jail_cell_test.go @@ -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 + } + } +} diff --git a/vendor/github.com/status-im/ottoext/cmd/ottoext/.gitignore b/vendor/github.com/status-im/ottoext/cmd/ottoext/.gitignore deleted file mode 100644 index c39f7d79f..000000000 --- a/vendor/github.com/status-im/ottoext/cmd/ottoext/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/ottoext diff --git a/vendor/github.com/status-im/ottoext/cmd/ottoext/example.js b/vendor/github.com/status-im/ottoext/cmd/ottoext/example.js deleted file mode 100644 index 29993f6c5..000000000 --- a/vendor/github.com/status-im/ottoext/cmd/ottoext/example.js +++ /dev/null @@ -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); - }); -}); diff --git a/vendor/github.com/status-im/ottoext/cmd/ottoext/main.go b/vendor/github.com/status-im/ottoext/cmd/ottoext/main.go deleted file mode 100644 index 00df3a413..000000000 --- a/vendor/github.com/status-im/ottoext/cmd/ottoext/main.go +++ /dev/null @@ -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) - } -} diff --git a/vendor/github.com/status-im/ottoext/repl/print.go b/vendor/github.com/status-im/ottoext/repl/print.go deleted file mode 100644 index 5ddf26961..000000000 --- a/vendor/github.com/status-im/ottoext/repl/print.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/status-im/ottoext/repl/repl.go b/vendor/github.com/status-im/ottoext/repl/repl.go deleted file mode 100644 index 0e0ea69d0..000000000 --- a/vendor/github.com/status-im/ottoext/repl/repl.go +++ /dev/null @@ -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 "" - } -}