├── LICENSE ├── README.md ├── go.mod ├── node.go └── node_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Josh Baker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-node 2 | 3 | [![GoDoc](https://godoc.org/github.com/tidwall/go-node?status.svg)](https://godoc.org/github.com/tidwall/go-node) 4 | 5 | Run Javascript in Go using Node.js. 6 | 7 | ## Installing 8 | 9 | ``` 10 | go get -u github.com/tidwall/go-node 11 | ``` 12 | 13 | ## Examples 14 | 15 | Create a Node.js VM and run some Javascript. 16 | 17 | ```go 18 | vm := node.New(nil) 19 | vm.Run("iLoveTheJS = true"); 20 | youLoveIt := vm.Run("iLoveTheJS") 21 | println(youLoveIt.String()) 22 | 23 | // output: 24 | // true 25 | ``` 26 | 27 | The `Run` function runs any Javascript code and returns a `Value` type that has 28 | two methods, `String` and `Error`. If the `Run` call caused a Javascript 29 | error then the `Error` method will contain that error. Otherwise, the `String` 30 | method will contain the Javascript return value. 31 | 32 | ```go 33 | vm := node.New(nil) 34 | vm.Run(` 35 | function greatScott(){ 36 | console.log('1.21 gigawatts?'); 37 | return 'Delorean go vroom'; 38 | } 39 | `); 40 | v := vm.Run("greatScott()"); 41 | if err := v.Error(); err != nil { 42 | log.Fatal(err) 43 | } 44 | println(v.String()) 45 | 46 | // output: 47 | // 1.21 gigawatts? 48 | // Deloran go vroom 49 | ``` 50 | 51 | You can "emit" messages from the Javascript to the Go. It's easy. Just use 52 | the Options.OnEmit method in Go, and call the `emit()` function from JS. 53 | 54 | ```go 55 | opts := node.Options { 56 | OnEmit: func(thing string) { 57 | println(thing) 58 | }, 59 | } 60 | vm := node.New(&opts) 61 | v := vm.Run("emit('a thing')") 62 | if err := v.Error(); err != nil{ 63 | log.Fatal(err) 64 | } 65 | 66 | // output: 67 | // a thing 68 | ``` 69 | 70 | Yay 🎉. Have fun now. 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/go-node 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package node 6 | 7 | import ( 8 | "bufio" 9 | "crypto/rand" 10 | "encoding/hex" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "net" 15 | "os" 16 | "os/exec" 17 | "runtime" 18 | "strings" 19 | "sync" 20 | "sync/atomic" 21 | ) 22 | 23 | // ErrThrown is returned where the input script from Run() throws a Javascript 24 | // error. 25 | type ErrThrown struct { 26 | err error 27 | } 28 | 29 | func (err ErrThrown) Error() string { 30 | return err.err.Error() 31 | } 32 | 33 | // VM is a Javascript Virtual Machine running on Node.js 34 | type VM interface { 35 | Run(javascript string) Value 36 | } 37 | 38 | // Value is the returned from Run(). 39 | type Value interface { 40 | // Error returns an error, if any. 41 | Error() error 42 | // String returns the string representation of the Value. 43 | String() string 44 | } 45 | 46 | // Options for VM 47 | type Options struct { 48 | // OnEmit is an optional callback that handles emitted messages. 49 | OnEmit func(thing string) 50 | // OnConsoleLog is an optional callback that handles notice messages that 51 | // are intended for the console logger. 52 | OnLog func(msg string) 53 | // OnConsoleError is an optional callback that handles error messages that 54 | // are intended for the console logger. 55 | OnError func(msg string) 56 | // Dir is the working directory for the VM. Default is the same working 57 | // directory and currently running Go process. 58 | Dir string 59 | // Flags are the additional startup flags that are provided to the 60 | // "node" process. 61 | Flags []string 62 | } 63 | 64 | // New returns a Javascript Virtual Machine running an isolated process of 65 | // Node.js. 66 | func New(opts *Options) VM { 67 | emit := func(thing string) {} 68 | var onError func(msg string) 69 | var onLog func(msg string) 70 | flags := []string{"--title", "go-node", "-e", jsRuntime} 71 | if opts != nil { 72 | emit = opts.OnEmit 73 | flags = append(flags, opts.Flags...) 74 | onError = opts.OnError 75 | onLog = opts.OnLog 76 | } 77 | cmd := exec.Command("node", flags...) 78 | if opts != nil && opts.Dir != "" { 79 | cmd.Dir = opts.Dir 80 | } 81 | stderr, _ := cmd.StderrPipe() 82 | stdout, _ := cmd.StdoutPipe() 83 | stdin, _ := cmd.StdinPipe() 84 | var uidb [16]byte 85 | rand.Read(uidb[:]) 86 | token := hex.EncodeToString(uidb[:]) 87 | rand.Read(uidb[:]) 88 | socket := os.TempDir() + "/" + hex.EncodeToString(uidb[:]) + ".sock" 89 | // Run stdout/stderr readers 90 | closech := make(chan bool, 2) 91 | var emsgmu sync.Mutex 92 | var emsgready bool 93 | var emsg string 94 | go func() { 95 | rd := bufio.NewReader(stderr) 96 | for { 97 | line, err := rd.ReadBytes('\n') 98 | if err != nil { 99 | break 100 | } 101 | emsgmu.Lock() 102 | if emsgready { 103 | if onError != nil { 104 | onError(string(line[:len(line)-1])) 105 | } else { 106 | os.Stderr.Write(line) 107 | } 108 | } else { 109 | emsg += string(line) 110 | } 111 | emsgmu.Unlock() 112 | } 113 | closech <- true 114 | }() 115 | go func() { 116 | rd := bufio.NewReader(stdout) 117 | for { 118 | line, err := rd.ReadBytes('\n') 119 | if err != nil { 120 | break 121 | } 122 | if strings.HasPrefix(string(line), token) { 123 | emit(string(line[len(token) : len(line)-1])) 124 | } else { 125 | if onLog != nil { 126 | onLog(string(line[:len(line)-1])) 127 | } else { 128 | os.Stdout.Write(line) 129 | } 130 | } 131 | } 132 | }() 133 | // Start the process 134 | if err := cmd.Start(); err != nil { 135 | return jsErrVM{err} 136 | } 137 | // Connect the Node instance. Perform a handshake. 138 | var conn net.Conn 139 | var rd *bufio.Reader 140 | if err := func() error { 141 | defer os.Remove(socket) 142 | ln, err := net.Listen("unix", socket) 143 | if err != nil { 144 | return err 145 | } 146 | _, err = fmt.Fprintf(stdin, "%s:%s\n", token, socket) 147 | if err != nil { 148 | return err 149 | } 150 | var done int32 151 | go func() { 152 | select { 153 | case <-closech: 154 | atomic.StoreInt32(&done, 1) 155 | ln.Close() 156 | } 157 | }() 158 | conn, err = ln.Accept() 159 | if err != nil { 160 | if atomic.LoadInt32(&done) == 1 { 161 | emsgmu.Lock() 162 | defer emsgmu.Unlock() 163 | emsg = strings.TrimSpace(emsg) 164 | if emsg != "" { 165 | return errors.New(emsg) 166 | } 167 | return errors.New("runtime failed to initiate") 168 | } 169 | return err 170 | } 171 | rd = bufio.NewReader(conn) 172 | line, err := rd.ReadBytes('\n') 173 | if err != nil { 174 | return err 175 | } 176 | parts := strings.Split(string(line), " ") 177 | if parts[0] != token { 178 | return errors.New("invalid handshake") 179 | } 180 | vers := strings.Split(parts[1][:len(parts[1])-1], ".") 181 | if !strings.HasPrefix(vers[0], "v") { 182 | return errors.New("invalid handshake") 183 | } 184 | emsgmu.Lock() 185 | emsgready = true 186 | emsgmu.Unlock() 187 | closech <- true 188 | return nil 189 | }(); err != nil { 190 | if conn != nil { 191 | conn.Close() 192 | } 193 | stdin.Close() 194 | return jsErrVM{err} 195 | } 196 | // Start the runtime loop 197 | ch := make(chan *jsValue) 198 | go func() { 199 | defer func() { 200 | stdin.Close() 201 | conn.Close() 202 | }() 203 | for { 204 | v := <-ch 205 | if v == nil { 206 | return 207 | } 208 | vals := []*jsValue{v} 209 | var done bool 210 | for !done { 211 | select { 212 | case v := <-ch: 213 | if v == nil { 214 | return 215 | } 216 | vals = append(vals, v) 217 | default: 218 | done = true 219 | } 220 | } 221 | var buf []byte 222 | for _, v := range vals { 223 | data, _ := json.Marshal(v.js) 224 | buf = append(buf, data...) 225 | buf = append(buf, '\n') 226 | v.js = "" // release the script 227 | } 228 | if _, err := conn.Write(buf); err != nil { 229 | for _, v := range vals { 230 | v.err = err 231 | v.wg.Done() 232 | v.vm = nil // release the vm 233 | } 234 | } else { 235 | for _, v := range vals { 236 | var msg string 237 | data, err := rd.ReadBytes('\n') 238 | if err != nil { 239 | v.err = err 240 | } else if err := json.Unmarshal(data, &msg); err != nil { 241 | v.err = err 242 | } else if msg != "" && msg[0] == 'e' { 243 | v.err = ErrThrown{fmt.Errorf("%s", msg[1:])} 244 | } else if msg != "" && msg[0] == 'v' { 245 | v.ret = string(msg[1:]) 246 | } else { 247 | v.err = errors.New("invalid response") 248 | } 249 | v.wg.Done() 250 | v.vm = nil // release the vm 251 | } 252 | } 253 | } 254 | }() 255 | vm := new(jsVM) 256 | vm.ch = ch 257 | runtime.SetFinalizer(vm, func(_ *jsVM) { ch <- nil }) 258 | return vm 259 | } 260 | 261 | type jsErrVM struct{ err error } 262 | type jsErrValue struct{ err error } 263 | 264 | func (vm jsErrVM) Run(js string) Value { 265 | return jsErrValue{vm.err} 266 | } 267 | 268 | func (vm jsErrValue) Error() error { 269 | return vm.err 270 | } 271 | 272 | func (vm jsErrValue) String() string { 273 | return "" 274 | } 275 | 276 | type jsVM struct { 277 | ch chan *jsValue 278 | } 279 | 280 | // Run some Javascript code. Returns the JSON or an error. 281 | func (vm *jsVM) Run(js string) Value { 282 | v := new(jsValue) 283 | v.vm = vm 284 | v.js = js 285 | v.wg.Add(1) 286 | vm.ch <- v 287 | return v 288 | } 289 | 290 | type jsValue struct { 291 | vm *jsVM 292 | js string 293 | wg sync.WaitGroup 294 | ret string 295 | err error 296 | } 297 | 298 | func (v *jsValue) Error() error { 299 | v.wg.Wait() 300 | return v.err 301 | } 302 | 303 | func (v *jsValue) String() string { 304 | v.wg.Wait() 305 | return v.ret 306 | } 307 | 308 | const jsRuntime = ` 309 | (function(){ 310 | let net = require('net'); 311 | let readline = require('readline'); 312 | let rl = readline.createInterface({input: process.stdin}); 313 | rl.on('line', function(line) { 314 | let socket = new net.Socket(); 315 | let token = line.split(":")[0]; 316 | socket.connect(line.split(":")[1], function() { 317 | socket.write(token+" "+process.version+"\n"); 318 | global.emit = function(arg) { 319 | console.log(token + arg); 320 | } 321 | let input = Buffer.alloc(0); 322 | let output = Buffer.alloc(0); 323 | socket.on("data", function(data) { 324 | input = Buffer.concat([input, data]); 325 | while (input.length > 0) { 326 | let idx = input.indexOf(10); 327 | if (idx == -1) { 328 | break; 329 | } 330 | let js = JSON.parse(input.slice(0, idx).toString('utf8')); 331 | input = input.slice(idx+1); 332 | if (input.length == 0) { 333 | input = Buffer.alloc(0); 334 | } 335 | let ret; 336 | try { 337 | ret = 'v'+eval.call(global, js); 338 | } catch (e) { 339 | ret = 'e'+e; 340 | } 341 | output = Buffer.concat([ 342 | output, 343 | Buffer.from(JSON.stringify(ret)+"\n", 'utf8') 344 | ]); 345 | } 346 | if (output.length > 0) { 347 | socket.write(output); 348 | output = output.slice(0, 0); 349 | } 350 | }); 351 | }); 352 | }); 353 | })(); 354 | ` 355 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Joshua J Baker. All rights reserved. 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package node 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestNode(t *testing.T) { 13 | ch := make(chan bool) 14 | vm := New(&Options{ 15 | OnEmit: func(arg string) { 16 | if arg != "100" { 17 | t.Fatalf("expected '%v', got '%v'", "100", arg) 18 | } else { 19 | ch <- true 20 | } 21 | }, 22 | }) 23 | vm.Run("x=0") 24 | N := 100 25 | for i := 0; i < N; i++ { 26 | val := vm.Run("++x") 27 | if err := val.Error(); err != nil { 28 | t.Fatal(err) 29 | } 30 | if val.String() != fmt.Sprint(i+1) { 31 | t.Fatalf("expected '%v', got '%v'", fmt.Sprint(i+1), val) 32 | } 33 | } 34 | val := vm.Run("x") 35 | if err := val.Error(); err != nil { 36 | t.Fatal(err) 37 | } 38 | if val.String() != fmt.Sprint(N) { 39 | t.Fatalf("expected '%v', got '%v'", fmt.Sprint(N), val) 40 | } 41 | vm.Run("emit(100)") 42 | <-ch 43 | 44 | v := vm.Run("throw new Error('hello')") 45 | if v.Error() == nil { 46 | t.Fatal("expected an error") 47 | } 48 | err, ok := v.Error().(ErrThrown) 49 | if !ok { 50 | t.Fatal("expected an ErrThrown") 51 | } 52 | if err.Error() != "Error: hello" { 53 | t.Fatalf("expected '%s', got '%s'", "Error: hello", err.Error()) 54 | } 55 | 56 | } 57 | --------------------------------------------------------------------------------