-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathjs_runner.go
232 lines (203 loc) · 6.46 KB
/
js_runner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Copyright 2012-Present Couchbase, Inc.
//
// Use of this software is governed by the Business Source License included
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
// in that file, in accordance with the Business Source License, use of this
// software will be governed by the Apache License, Version 2.0, included in
// the file licenses/APL2.txt.
package sgbucket
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/robertkrimen/otto"
)
// Alternate type to wrap a Go string in to mark that Call() should interpret it as JSON.
// That is, when Call() sees a parameter of type JSONString it will parse the JSON and use
// the result as the parameter value, instead of just converting it to a JS string.
type JSONString string
type NativeFunction func(otto.FunctionCall) otto.Value
// This specific instance will be returned if a call times out.
var ErrJSTimeout = errors.New("javascript function timed out")
// Go interface to a JavaScript function (like a map/reduce/channelmap/validation function.)
// Each JSServer object compiles a single function into a JavaScript runtime, and lets you
// call that function.
// JSRunner is NOT thread-safe! For that, use JSServer, a wrapper around it.
type JSRunner struct {
js *otto.Otto
fn otto.Value
fnSource string
timeout time.Duration
// Optional function that will be called just before the JS function.
Before func()
// Optional function that will be called after the JS function returns, and can convert
// its output from JS (Otto) values to Go values.
After func(otto.Value, error) (interface{}, error)
}
// Creates a new JSRunner that will run a JavaScript function.
// 'funcSource' should look like "function(x,y) { ... }"
func NewJSRunner(funcSource string, timeout time.Duration) (*JSRunner, error) {
runner := &JSRunner{}
if err := runner.Init(funcSource, timeout); err != nil {
return nil, err
}
return runner, nil
}
// Initializes a JSRunner.
func (runner *JSRunner) Init(funcSource string, timeout time.Duration) error {
return runner.InitWithLogging(funcSource, timeout, defaultLogFunction, defaultLogFunction)
}
func (runner *JSRunner) InitWithLogging(funcSource string, timeout time.Duration, consoleErrorFunc func(string), consoleLogFunc func(string)) error {
runner.js = otto.New()
runner.fn = otto.UndefinedValue()
runner.timeout = timeout
runner.DefineNativeFunction("log", func(call otto.FunctionCall) otto.Value {
var output string
for _, arg := range call.ArgumentList {
str, _ := arg.ToString()
output += str + " "
}
logg("JS: %s", output)
return otto.UndefinedValue()
})
if _, err := runner.SetFunction(funcSource); err != nil {
return err
}
return runner.js.Set("console", map[string]interface{}{
"error": consoleErrorFunc,
"log": consoleLogFunc,
})
}
func defaultLogFunction(s string) {
fmt.Println(s)
}
// Sets the JavaScript function the runner executes.
func (runner *JSRunner) SetFunction(funcSource string) (bool, error) {
if funcSource == runner.fnSource {
return false, nil // no-op
}
if funcSource == "" {
runner.fn = otto.UndefinedValue()
} else {
fnobj, err := runner.js.Object("(" + funcSource + ")")
if err != nil {
return false, err
}
if fnobj.Class() != "Function" {
return false, errors.New("JavaScript source does not evaluate to a function")
}
runner.fn = fnobj.Value()
}
runner.fnSource = funcSource
return true, nil
}
// Sets the runner's timeout. A value of 0 removes any timeout.
func (runner *JSRunner) SetTimeout(timeout time.Duration) {
runner.timeout = timeout
}
// Lets you define native helper functions (for example, the "emit" function to be called by
// JS map functions) in the main namespace of the JS runtime.
// This method is not thread-safe and should only be called before making any calls to the
// main JS function.
func (runner *JSRunner) DefineNativeFunction(name string, function NativeFunction) {
_ = runner.js.Set(name, (func(otto.FunctionCall) otto.Value)(function))
}
func (runner *JSRunner) jsonToValue(jsonStr string) (interface{}, error) {
if jsonStr == "" {
return otto.NullValue(), nil
}
var parsed interface{}
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
return nil, fmt.Errorf("Unparseable JSRunner input: %s", jsonStr)
}
return parsed, nil
}
// ToValue calls ToValue on the otto instance. Required for conversion of
// complex types to otto Values.
func (runner *JSRunner) ToValue(value interface{}) (otto.Value, error) {
return runner.js.ToValue(value)
}
// Invokes the JS function with JSON inputs.
func (runner *JSRunner) CallWithJSON(inputs ...string) (interface{}, error) {
if runner.Before != nil {
runner.Before()
}
var result otto.Value
var err error
if runner.fn.IsUndefined() {
result = otto.UndefinedValue()
} else {
inputJS := make([]interface{}, len(inputs))
for i, inputStr := range inputs {
inputJS[i], err = runner.jsonToValue(inputStr)
if err != nil {
return nil, err
}
}
result, err = runner.fn.Call(runner.fn, inputJS...)
}
if runner.After != nil {
return runner.After(result, err)
}
return nil, err
}
// Invokes the JS function with Go inputs.
func (runner *JSRunner) Call(ctx context.Context, inputs ...interface{}) (_ interface{}, err error) {
if runner.Before != nil {
runner.Before()
}
var result otto.Value
if runner.fn.IsUndefined() {
result = otto.UndefinedValue()
} else {
inputJS := make([]interface{}, len(inputs))
for i, input := range inputs {
if jsonStr, ok := input.(JSONString); ok {
if input, err = runner.jsonToValue(string(jsonStr)); err != nil {
return nil, err
}
}
inputJS[i], err = runner.js.ToValue(input)
if err != nil {
return nil, fmt.Errorf("Couldn't convert %#v to JS: %s", input, err)
}
}
var completed chan struct{}
timeout := runner.timeout
if timeout > 0 {
completed = make(chan struct{})
defer func() {
if caught := recover(); caught != nil {
if caught == ErrJSTimeout {
err = ErrJSTimeout
return
}
panic(caught)
}
}()
runner.js.Interrupt = make(chan func(), 1)
timer := time.NewTimer(timeout)
go func() {
defer timer.Stop()
select {
case <-completed:
return
case <-timer.C:
runner.js.Interrupt <- func() {
panic(ErrJSTimeout)
}
}
}()
}
result, err = runner.fn.Call(runner.fn, inputJS...)
if completed != nil {
close(completed)
}
}
if runner.After != nil {
return runner.After(result, err)
}
return nil, err
}