Browse Source

Resolve Promises with AsyncResult, work on net node_compat

pull/1/head
Tyler Sommer 12 months ago
parent
commit
e9caee8cb4
Signed by: tyler-sommer GPG Key ID: C09C010500DBD008
30 changed files with 2690 additions and 139 deletions
  1. +1
    -0
      .dockerignore
  2. +3
    -0
      .gitignore
  3. +9
    -0
      Makefile
  4. +1
    -0
      go.mod
  5. +2
    -0
      go.sum
  6. +5
    -0
      plugin/plugin.go
  7. +3
    -4
      plugins/babel/plugin.go
  8. +2
    -2
      plugins/babel/transformer/babel.go
  9. +71
    -0
      plugins/babel/transformer/babel_test.go
  10. +3
    -1
      plugins/node_compat/child_process.go
  11. +5
    -2
      plugins/node_compat/crypto.go
  12. +15
    -2
      plugins/node_compat/event_emitter.go
  13. +2
    -1
      plugins/node_compat/exec.go
  14. +120
    -0
      plugins/node_compat/http.go
  15. +166
    -0
      plugins/node_compat/internal/net.go
  16. +225
    -0
      plugins/node_compat/net.go
  17. +173
    -0
      plugins/node_compat/net_test.go
  18. +20
    -10
      plugins/node_compat/plugin.go
  19. +142
    -0
      plugins/node_compat/stream.go
  20. +1
    -3
      plugins/script/plugin.go
  21. +1
    -3
      plugins/squircy2_compat/plugin.go
  22. +0
    -4
      plugins/squircy2_compat/runtime.go
  23. +1174
    -0
      testdata/es6-promise.js
  24. +15
    -0
      testdata/package.json
  25. +4
    -1
      vm/require.go
  26. +354
    -0
      vm/result.go
  27. +85
    -0
      vm/result_test.go
  28. +43
    -11
      vm/scheduler.go
  29. +17
    -95
      vm/vm.go
  30. +28
    -0
      vm/vm_test.go

+ 1
- 0
.dockerignore View File

@ -1,3 +1,4 @@
out/*
node_modules/*
testdata/node_modules/*
Dockerfile

+ 3
- 0
.gitignore View File

@ -3,5 +3,8 @@
!.dockerignore
out/*
node_modules/*
testdata/node_modules/*
testdata/yarn-error.log
testdata/yarn.lock
yarn.lock
package-lock.json

+ 9
- 0
Makefile View File

@ -13,6 +13,8 @@ OUTPUT_BASE := out
PLUGIN_TARGETS := $(patsubst %,$(OUTPUT_BASE)/%.so,$(PLUGINS))
SQUIRCY_TARGET := $(OUTPUT_BASE)/squircy
TESTDATA_NODEMODS_TARGET := testdata/node_modules
.PHONY: all build generate run squircy plugins clean
all: build
@ -31,6 +33,13 @@ plugins: $(PLUGIN_TARGETS)
run: build
$(SQUIRCY_TARGET)
test: $(TESTDATA_NODEMODS_TARGET)
go test --tags netgo -race ./...
$(TESTDATA_NODEMODS_TARGET):
cd testdata && \
yarn install
.SECONDEXPANSION:
$(PLUGIN_TARGETS): $(OUTPUT_BASE)/%.so: $$(wildcard plugins/%/*) $(SOURCES)
go build -tags netgo -race -o $@ -buildmode=plugin plugins/$*/*.go


+ 1
- 0
go.mod View File

@ -6,6 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733
github.com/dop251/goja_nodejs v0.0.0-20200128125109-2d688c7e0ac4 // indirect
github.com/fatih/structtag v1.0.0
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
github.com/kr/pretty v0.1.0 // indirect


+ 2
- 0
go.sum View File

@ -7,6 +7,8 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw=
github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dop251/goja_nodejs v0.0.0-20200128125109-2d688c7e0ac4 h1:L3xoE2+R67n8YEoNBB9K5h9CYJd3QbD8iYAjnuqFNK8=
github.com/dop251/goja_nodejs v0.0.0-20200128125109-2d688c7e0ac4/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/fatih/structtag v1.0.0 h1:pTHj65+u3RKWYPSGaU290FpI/dXxTaHdVwVwbcPKmEc=
github.com/fatih/structtag v1.0.0/go.mod h1:IKitwq45uXL/yqi5mYghiD3w9H6eTOvI9vnk8tXMphA=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=


+ 5
- 0
plugin/plugin.go View File

@ -1,6 +1,7 @@
package plugin // import "code.dopame.me/veonik/squircy3/plugin"
import (
"fmt"
"plugin"
"sync"
@ -140,3 +141,7 @@ func (m *Manager) Configure() []error {
}
return errs
}
func Main(pluginName string) {
fmt.Println(pluginName, "- a plugin for squircy3")
}

+ 3
- 4
plugins/babel/plugin.go View File

@ -1,10 +1,9 @@
package main // import "code.dopame.me/veonik/squircy3/plugins/babel"
import (
"fmt"
"code.dopame.me/veonik/squircy3/config"
"code.dopame.me/veonik/squircy3/plugin"
babel "code.dopame.me/veonik/squircy3/plugins/babel/transformer"
"code.dopame.me/veonik/squircy3/vm"
"github.com/dop251/goja"
@ -15,7 +14,7 @@ import (
const pluginName = "babel"
func main() {
fmt.Println(pluginName, "- a plugin for squircy3")
plugin.Main(pluginName)
}
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
@ -53,7 +52,7 @@ func (p *babelPlugin) HandleRuntimeInit(gr *goja.Runtime) {
return
}
p.vm.SetTransformer(nil)
b, err := NewBabel(gr)
b, err := babel.New(gr)
if err != nil {
logrus.Warnln("unable to run babel init script:", err)
return


plugins/babel/babel.go → plugins/babel/transformer/babel.go View File


+ 71
- 0
plugins/babel/transformer/babel_test.go View File

@ -0,0 +1,71 @@
package transformer_test
import (
"os"
"testing"
"github.com/dop251/goja"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squircy3/plugins/babel/transformer"
"code.dopame.me/veonik/squircy3/vm"
)
func init() {
if _, err := os.Stat("../../../testdata/node_modules"); os.IsNotExist(err) {
panic("tests in this package require node dependencies to be installed in the testdata directory")
}
}
func HandleRuntimeInit(vmp *vm.VM) func(*goja.Runtime) {
return func(gr *goja.Runtime) {
vmp.SetTransformer(nil)
b, err := transformer.New(gr)
if err != nil {
logrus.Warnln("unable to run babel init script:", err)
return
}
vmp.SetTransformer(b.Transform)
}
}
var registry = vm.NewRegistry("../../../testdata")
func TestBabel_Transform(t *testing.T) {
vmp, err := vm.New(registry)
if err != nil {
t.Fatalf("unexpected error creating VM: %s", err)
}
// vmp.SetModule(&vm.Module{Name: "events", Path: "./events.js", Main: "index"})
vmp.OnRuntimeInit(HandleRuntimeInit(vmp))
if err = vmp.Start(); err != nil {
t.Fatalf("unexpected error starting VM: %s", err)
}
res, err := vmp.RunString(`require('regenerator-runtime');
(async () => {
let output = null;
setTimeout(() => {
output = "HELLO!";
}, 200);
const sleep = async (d) => {
return new Promise(resolve => {
setTimeout(() => resolve(), d);
});
};
await sleep(500);
return output;
})();
`).Await()
if err != nil {
t.Fatalf("error requiring module: %s", err)
}
expected := "HELLO!"
if res.String() != expected {
t.Fatalf("expected: %s\ngot: %s", expected, res.String())
}
}

+ 3
- 1
plugins/node_compat/child_process.go View File

@ -2,9 +2,11 @@ package main
import "code.dopame.me/veonik/squircy3/vm"
var childProcess = &vm.Module{
// Module ChildProcess is a polyfill for the child_process node module.
var ChildProcess = &vm.Module{
Name: "child_process",
Main: "index",
Path: "child_process",
Body: `
import EventEmitter from 'events';


+ 5
- 2
plugins/node_compat/crypto.go View File

@ -2,9 +2,12 @@ package main
import "code.dopame.me/veonik/squircy3/vm"
var crypto = &vm.Module{
// Module Crypto is a polyfill providing some functionality from the node
// crypto module.
var Crypto = &vm.Module{
Name: "crypto",
Main: "index",
Path: "crypto",
Body: `
import {Buffer} from 'buffer';
@ -36,4 +39,4 @@ export const createHash = (kind) => {
throw new Error('unsupported hash algo: '+kind);
}
};`,
}
}

+ 15
- 2
plugins/node_compat/event_emitter.go View File

@ -2,14 +2,20 @@ package main
import "code.dopame.me/veonik/squircy3/vm"
var eventEmitter = &vm.Module{
// Module EventEmitter is a polyfill for the node events module.
// See https://gist.github.com/mudge/5830382
// Modified to add listenerCount instance method.
var EventEmitter = &vm.Module{
Name: "events",
Main: "index",
Path: "events",
Body: `/* Polyfill EventEmitter. */
var EventEmitter = function () {
this.events = {};
};
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype.on = function (event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
@ -22,7 +28,7 @@ EventEmitter.prototype.removeListener = function (event, listener) {
var idx;
if (typeof this.events[event] === 'object') {
idx = indexOf(this.events[event], listener);
idx = this.events[event].indexOf(listener);
if (idx > -1) {
this.events[event].splice(idx, 1);
@ -50,5 +56,12 @@ EventEmitter.prototype.once = function (event, listener) {
});
};
EventEmitter.prototype.listenerCount = function (event) {
if(!this.events[event]) {
return 0;
}
return this.events[event].length;
};
module.exports = EventEmitter;`,
}

+ 2
- 1
plugins/node_compat/exec.go View File

@ -14,6 +14,7 @@ import (
"github.com/sirupsen/logrus"
)
// A safeBuffer is a buffer that is safe to use in concurrent contexts.
type safeBuffer struct {
buf bytes.Buffer
mu sync.RWMutex
@ -61,7 +62,7 @@ func NewProcess(command string, args ...string) *Process {
return p
}
// Start starts the process, leaving stdin open for writing.
// start starts the process, leaving stdin open for writing.
//
// If the started process reads from stdin, it may not exit until
// CloseInput is called.


+ 120
- 0
plugins/node_compat/http.go View File

@ -0,0 +1,120 @@
package main
import "code.dopame.me/veonik/squircy3/vm"
// Module Http is a polyfill for the node http module.
var Http = &vm.Module{
Name: "http",
Main: "index",
Path: "http",
Body: `
import EventEmitter from 'events';
import {Server as NetServer} from 'net';
export class Server extends NetServer {
constructor(options, requestListener) {}
setTimeout(timeout, callback = null) {}
get maxHeadersCount() {}
get timeout() {}
get headersTimeout() {}
get keepAliveTimeout() {}
}
export class OutgoingMessage {
get upgrading() {}
get chunkedEncoding() {}
get shouldKeepAlive() {}
get useChunkedEncodingByDefault() {}
get sendDate() {}
get finished() {}
get headersSent() {}
get connection() {}
constructor() {}
setTimeout(timeout, callback = null) {}
setHeader(name, value) {}
getHeader(name) {}
getHeaders() {}
getHeaderNames() {}
hasHeader(name) {}
removeHeader(name) {}
addTrailers(headers) {}
flushHeaders() {}
}
export class ServerResponse extends OutgoingMessage {
get statusCode() {}
get statusMessage() {}
constructor(req) {}
assignSocket(socket) {}
detachSocket(socket) {}
writeContinue(callback) {}
writeHead(statusCode, reasonPhrase, headers = null) {}
}
export class ClientRequest extends OutgoingMessage {
get connection() {}
get socket() {}
get aborted() {}
constructor(uri, callback = null) {}
get path() {}
abort() {}
onSocket(socket) {}
setTimeout(timeout, callback = null) {}
setNoDelay(noDelay) {}
setSocketKeepAlive(enable, initialDelay = null) {}
}
class IncomingMessage {
constructor(socket) {}
get httpVersion() {}
get httpVersionMajor() {}
get httpVersionMinor() {}
get connection() {}
get headers() {}
get rawHeaders() {}
get trailers() {}
get rawTrailers() {}
setTimeout(timeout, callback = null) {}
get method() {}
get url() {}
get statusCode() {}
get statusMessage() {}
get socket() {}
destroy() {}
}
class Agent {
get maxFreeSockets() {
}
get maxSockets() {}
get sockets() {}
get requests() {}
constructor(options) {}
destroy() {}
}
export const METHODS = [];
export const STATUS_CODES = {};
export function createServer(options, requestListener) {}
export function request(options, callback) {}
export function get(options, callback) {}
export let globalAgent;
export const maxHeaderSize;
`,
}

+ 166
- 0
plugins/node_compat/internal/net.go View File

@ -0,0 +1,166 @@
package internal
import (
"io"
"net"
"sync"
"github.com/dop251/goja"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"code.dopame.me/veonik/squircy3/vm"
)
type NetConn struct {
conn net.Conn
buf []byte
readable bool
}
type readResult struct {
ready bool
value string
error error
mu sync.Mutex
}
func (r *readResult) Ready() bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.ready
}
func (r *readResult) Value() (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
if !r.ready {
return "", errors.New("not ready")
}
return r.value, r.error
}
func (r *readResult) resolve(val string, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.ready {
return
}
r.value = val
r.error = err
r.ready = true
}
func NewNetConn(conn net.Conn) (*NetConn, error) {
return &NetConn{
conn: conn,
buf: make([]byte, 1024),
readable: true,
}, nil
}
func (c *NetConn) Write(s string) (n int, err error) {
return c.conn.Write([]byte(s))
}
// Read asynchronously reads from the connection.
// A readResult is returned back to the js vm and once the read completes,
// it can be read from. This allows the js vm to avoid blocking for reads.
func (c *NetConn) Read(_ int) *readResult {
res := &readResult{}
if !c.readable {
logrus.Warnln("reading from unreadable conn")
return res
}
go func() {
n, err := c.conn.Read(c.buf)
if err != nil {
if err != io.EOF {
res.resolve("", err)
return
} else {
// on the next call to read, we'll return nil to signal done.
c.readable = false
}
}
logrus.Warnln("read", n, "bytes")
rb := make([]byte, n)
copy(rb, c.buf)
res.resolve(string(rb), nil)
}()
return res
}
func (c *NetConn) Close() error {
return c.conn.Close()
}
func (c *NetConn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *NetConn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func Dial(kind, addr string) (*NetConn, error) {
c, err := net.Dial(kind, addr)
if err != nil {
return nil, err
}
return NewNetConn(c)
}
type Server struct {
listener net.Listener
vm *vm.VM
onConnect goja.Callable
}
func (s *Server) accept() {
for {
conn, err := s.listener.Accept()
if err != nil {
logrus.Warnln("failed to accept new connection", err)
return
}
s.vm.Do(func(gr *goja.Runtime) {
nc, err := NewNetConn(conn)
if err != nil {
logrus.Warnln("failed to get NetConn from net.Conn", err)
return
}
if _, err := s.onConnect(nil, gr.ToValue(nc)); err != nil {
logrus.Warnln("error running on-connect callback", err)
}
})
}
}
func (s *Server) Close() error {
defer func() {
s.onConnect = nil
}()
return s.listener.Close()
}
func (s *Server) Addr() net.Addr {
return s.listener.Addr()
}
func Listen(vmp *vm.VM, onConnect goja.Callable, kind, addr string) (*Server, error) {
l, err := net.Listen(kind, addr)
if err != nil {
return nil, err
}
s := &Server{
listener: l,
vm: vmp,
onConnect: onConnect,
}
go s.accept()
return s, nil
}

+ 225
- 0
plugins/node_compat/net.go View File

@ -0,0 +1,225 @@
package main
import "code.dopame.me/veonik/squircy3/vm"
// Module Net is a polyfill for the node net module.
var Net = &vm.Module{
Name: "net",
Main: "index",
Path: "net",
Body: `
import {Duplex} from 'stream';
import {Buffer} from 'buffer';
import {EventEmitter} from 'events';
const goAddrToNode = addr => {
// todo: support ipv6 addresses, udp, ipc, etc
let parts = addr.String().split(':');
return {
host: parts[0],
port: parseInt(parts[1]),
family: addr.Network(),
};
};
export class Socket extends Duplex {
constructor(options = {}) {
super(options);
this.options = options || {};
this._connection = null;
this._local = null;
this._remote = null;
this._connecting = false;
this.on('ready', () => {
this._connecting = false;
this._local = goAddrToNode(this._connection.LocalAddr());
this._remote = goAddrToNode(this._connection.RemoteAddr());
});
}
_read(size = null) {
if(!this._connection) {
return;
}
let result = this._connection.Read(size);
let wait = 1;
let check = () => {
if(result.Ready()) {
let data = result.Value();
if(data !== null && data.length) {
this.push(data);
} else {
this.push(null);
}
} else {
if(wait < 64) {
wait *= 2;
}
setTimeout(check, wait);
}
};
check();
}
_write(buffer, encoding, callback) {
if(!this._connection) {
callback(Error('not connected'));
return;
}
let err = null;
try {
this._connection.Write(buffer);
} catch(e) {
err = e;
} finally {
callback(err);
}
}
async connect(options, listener = null) {
// todo: support ipc
// todo: udp is defined in Node's dgram module
if(listener !== null) {
this.once('connect', listener);
}
let host = options.host || 'localhost';
let port = options.port;
if(!port) {
throw new Error('ipc connections are unsupported');
}
console.log('dialing', host + ':' + port);
this._connecting = true;
this._connection = internal.Dial('tcp', host + ':' + port);
this.emit('connect');
this.emit('ready');
}
setEncoding(encoding) {
this.encoding = encoding;
}
_destroy(callback) {
let err = null;
try {
this._connection.Close();
} catch(e) {
err = e;
console.log('error destroying', err.toString());
} finally {
this._connection = null;
if(callback) {
callback(err);
}
}
}
// setTimeout(timeout, callback = null) {}
// setNoDelay(noDelay) {}
// setKeepAlive(keepAlive) {}
// address() {}
// unref() {}
// ref() {}
//
// get bufferSize() {}
// get bytesRead() {}
// get bytesWritten() {}
get connecting() {
return this._connecting;
}
get localAddress() {
if(!this._connection) {
return null;
}
return this._local.host;
}
get localPort() {
if(!this._connection) {
return null;
}
return this._local.port;
}
get remoteAddress() {
if(!this._connection) {
return null;
}
return this._remote.host;
}
get remoteFamily() {
if(!this._connection) {
return null;
}
return this._remote.family;
}
get remotePort() {
if(!this._connection) {
return null;
}
return this._remote.port;
}
}
export class Server extends EventEmitter {
constructor(listener = null) {
super();
if(listener !== null) {
this.on('connection', listener);
}
this._server = null;
}
listen(port, hostname, listener = null) {
if(listener !== null) {
this.on('connection', listener);
}
let addr = hostname + ':' + port;
let accept = (conn) => {
let socket = new Socket();
socket._connection = conn;
socket.on('end', () => {
console.log('server ended');
socket.destroy();
});
socket.emit('connect');
this.emit('connection', socket);
socket.emit('ready');
};
this._server = internal.Listen(accept, 'tcp4', addr);
this.emit('listening');
}
close(callback = null) {
this._server.Close();
this.emit('close');
if(callback !== null) {
callback();
}
}
address() {
return goAddrToNode(this._server.Addr());
}
getConnections(callback) {}
ref() {}
unref() {}
get maxConnections() {}
get connections() {}
get listening() {}
}
//
// export function createServer(options = null, connectionListener = null) {}
//
// export function connect(options, connectionListener = null) {}
//
// export function createConnection(options, connectionListener = null) {}
//
// export function isIP(input) {}
//
// export function isIPv4(input) {}
//
// export function isIPv6(input) {}
`,
}

+ 173
- 0
plugins/node_compat/net_test.go View File

@ -0,0 +1,173 @@
package main_test
import (
"crypto/sha1"
"fmt"
"os"
"testing"
"github.com/dop251/goja"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
babel "code.dopame.me/veonik/squircy3/plugins/babel/transformer"
node_compat "code.dopame.me/veonik/squircy3/plugins/node_compat"
"code.dopame.me/veonik/squircy3/plugins/node_compat/internal"
"code.dopame.me/veonik/squircy3/vm"
)
func init() {
if _, err := os.Stat("../../testdata/node_modules"); os.IsNotExist(err) {
panic("tests in this package require node dependencies to be installed in the testdata directory")
}
}
func HandleRuntimeInit(vmp *vm.VM) func(*goja.Runtime) {
return func(gr *goja.Runtime) {
vmp.SetTransformer(nil)
b, err := babel.New(gr)
if err != nil {
logrus.Warnln("unable to run babel init script:", err)
return
}
vmp.SetTransformer(b.Transform)
v := gr.NewObject()
if err := v.Set("Sum", func(b []byte) (string, error) {
return fmt.Sprintf("%x", sha1.Sum(b)), nil
}); err != nil {
logrus.Warnf("%s: error initializing runtime: %s", node_compat.PluginName, err)
}
gr.Set("sha1", v)
v = gr.NewObject()
if err := v.Set("Dial", internal.Dial); err != nil {
logrus.Warnf("%s: error initializing runtime: %s", node_compat.PluginName, err)
}
if err := v.Set("Listen", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 3 {
panic(gr.NewGoError(errors.New("expected exactly 3 arguments")))
}
arg0 := call.Arguments[0]
fn, ok := goja.AssertFunction(arg0)
if !ok {
panic(gr.NewGoError(errors.New("expected argument 0 to be callable")))
}
kind := call.Arguments[1].String()
addr := call.Arguments[2].String()
srv, err := internal.Listen(vmp, fn, kind, addr)
if err != nil {
panic(gr.NewGoError(err))
}
return gr.ToValue(srv)
}); err != nil {
logrus.Warnf("%s: error initializing runtime: %s", node_compat.PluginName, err)
}
gr.Set("internal", v)
_, err = gr.RunString(`this.global = this.global || this;
require('core-js-bundle');
this.process = this.process || require('process/browser');
require('regenerator-runtime');`)
if err != nil {
logrus.Warnf("%s: error initializing runtime: %s", node_compat.PluginName, err)
}
}
}
var registry = vm.NewRegistry("../../testdata")
func TestNodeCompat_Net(t *testing.T) {
vmp, err := vm.New(registry)
if err != nil {
t.Fatalf("unexpected error creating VM: %s", err)
}
vmp.SetModule(node_compat.EventEmitter)
vmp.SetModule(node_compat.ChildProcess)
vmp.SetModule(node_compat.Crypto)
vmp.SetModule(node_compat.Stream)
vmp.SetModule(node_compat.Net)
vmp.SetModule(node_compat.Http)
vmp.OnRuntimeInit(HandleRuntimeInit(vmp))
if err = vmp.Start(); err != nil {
t.Fatalf("unexpected error starting VM: %s", err)
}
res, err := vmp.RunString(`
import {Socket, Server} from 'net';
const sleep = async (d) => {
return new Promise(resolve => {
setTimeout(() => resolve(), d);
});
};
let resolve;
let output = '';
let result = new Promise(_resolve => {
resolve = _resolve;
});
// let originalLog = console.log;
// console.log = function log() {
// let args = Array.from(arguments).map(arg => arg.toString());
// originalLog(args.join(' '));
// };
(async () => {
var srv = new Server();
srv.listen(3333, 'localhost', async conn => {
console.log('connected');
conn.on('data', data => {
console.log('server received', data.toString());
});
conn.on('close', () => console.log('server side disconnected'));
conn.on('end', () => {
console.log('ending server connection from user code!');
srv.close();
});
conn.on('ready', () => {
console.log('server: ' + conn.localAddress + ':' + conn.localPort);
console.log('client: ' + conn.remoteAddress + ':' + conn.remotePort);
});
conn.write('hi');
await sleep(500);
conn.write('exit\n');
});
srv.on('close', () => {
resolve(output);
});
console.log('listening on', srv.address());
})();
(async () => {
let sock = new Socket();
console.log('wot');
sock.on('data', d => {
let data = d.toString();
console.log('received', data);
if(data.replace(/\n$/, '') === 'exit') {
sock.end('peace!');
sock.destroy();
return;
} else {
output += d;
}
});
sock.on('close', () => console.log('client side disconnected'));
await sock.connect({host: 'localhost', port: 3333});
sock.write('hello there!\r\n');
console.log('wot2');
})();
result;
`).Await()
if err != nil {
t.Fatalf("error requiring module: %s", err)
}
expected := "hi"
if res.String() != expected {
t.Fatalf("expected: %s\ngot: %s", expected, res.String())
}
}

+ 20
- 10
plugins/node_compat/plugin.go View File

@ -10,23 +10,27 @@ import (
"code.dopame.me/veonik/squircy3/config"
"code.dopame.me/veonik/squircy3/plugin"
"code.dopame.me/veonik/squircy3/plugins/node_compat/internal"
"code.dopame.me/veonik/squircy3/vm"
)
const pluginName = "node_compat"
const PluginName = "node_compat"
func main() {
fmt.Println(pluginName, "- a plugin for squircy3")
plugin.Main(PluginName)
}
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
vmp, err := vm.FromPlugins(m)
if err != nil {
return nil, errors.Wrapf(err, "%s: required dependency missing (vm)", pluginName)
return nil, errors.Wrapf(err, "%s: required dependency missing (vm)", PluginName)
}
vmp.SetModule(eventEmitter)
vmp.SetModule(childProcess)
vmp.SetModule(crypto)
vmp.SetModule(EventEmitter)
vmp.SetModule(ChildProcess)
vmp.SetModule(Crypto)
vmp.SetModule(Stream)
vmp.SetModule(Net)
vmp.SetModule(Http)
return &nodeCompatPlugin{}, nil
}
@ -40,7 +44,7 @@ func (p *nodeCompatPlugin) HandleRuntimeInit(r *goja.Runtime) {
}
v := r.NewObject()
if err := v.Set("Command", NewProcess); err != nil {
logrus.Warnf("%s: error initializing runtime: %s", pluginName, err)
logrus.Warnf("%s: error initializing runtime: %s", PluginName, err)
}
r.Set("exec", v)
@ -48,16 +52,22 @@ func (p *nodeCompatPlugin) HandleRuntimeInit(r *goja.Runtime) {
if err := v.Set("Sum", func(b []byte) (string, error) {
return fmt.Sprintf("%x", sha1.Sum(b)), nil
}); err != nil {
logrus.Warnf("%s: error initializing runtime: %s", pluginName, err)
logrus.Warnf("%s: error initializing runtime: %s", PluginName, err)
}
r.Set("sha1", v)
v = r.NewObject()
if err := v.Set("Dial", internal.Dial); err != nil {
logrus.Warnf("%s: error initializing runtime: %s", PluginName, err)
}
r.Set("internal", v)
_, err := r.RunString(`this.global = this.global || this;
require('core-js-bundle');
this.process = this.process || require('process/browser');
require('regenerator-runtime');`)
if err != nil {
logrus.Warnf("%s: error initializing runtime: %s", pluginName, err)
logrus.Warnf("%s: error initializing runtime: %s", PluginName, err)
}
}
@ -71,5 +81,5 @@ func (p *nodeCompatPlugin) Configure(conf config.Config) error {
}
func (p *nodeCompatPlugin) Name() string {
return pluginName
return PluginName
}

+ 142
- 0
plugins/node_compat/stream.go View File

@ -0,0 +1,142 @@
package main
import (
"code.dopame.me/veonik/squircy3/vm"
)
// Module Stream is based on stream-browserify and relies on readable-stream.
// See https://github.com/browserify/stream-browserify/blob/v3.0.0/index.js
var Stream = &vm.Module{
Name: "stream",
Main: "index.js",
Path: "stream",
Body: `// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
module.exports = Stream;
var EE = require('events').EventEmitter;
var inherits = require('inherits');
inherits(Stream, EE);
Stream.Readable = require('readable-stream/lib/_stream_readable.js');
Stream.Writable = require('readable-stream/lib/_stream_writable.js');
Stream.Duplex = require('readable-stream/lib/_stream_duplex.js');
Stream.Transform = require('readable-stream/lib/_stream_transform.js');
Stream.PassThrough = require('readable-stream/lib/_stream_passthrough.js');
Stream.finished = require('readable-stream/lib/internal/streams/end-of-Stream.js')
Stream.pipeline = require('readable-stream/lib/internal/streams/pipeline.js')
// Backwards-compat with node 0.4.x
Stream.Stream = Stream;
// old-style streams. Note that the pipe method (the only relevant
// part of this class) is overridden in the Readable class.
function Stream() {
EE.call(this);
}
Stream.prototype.pipe = function(dest, options) {
var source = this;
function ondata(chunk) {
if (dest.writable) {
if (false === dest.write(chunk) && source.pause) {
source.pause();
}
}
}
source.on('data', ondata);
function ondrain() {
if (source.readable && source.resume) {
source.resume();
}
}
dest.on('drain', ondrain);
// If the 'end' option is not supplied, dest.end() will be called when
// source gets the 'end' or 'close' events. Only dest.end() once.
if (!dest._isStdio && (!options || options.end !== false)) {
source.on('end', onend);
source.on('close', onclose);
}
var didOnEnd = false;
function onend() {
if (didOnEnd) return;
didOnEnd = true;
dest.end();
}
function onclose() {
if (didOnEnd) return;
didOnEnd = true;
if (typeof dest.destroy === 'function') dest.destroy();
}
// don't leave dangling pipes when there are errors.
function onerror(er) {
cleanup();
if (EE.listenerCount(this, 'error') === 0) {
throw er; // Unhandled Stream error in pipe.
}
}
source.on('error', onerror);
dest.on('error', onerror);
// remove all the event listeners that were added.
function cleanup() {
source.removeListener('data', ondata);
dest.removeListener('drain', ondrain);
source.removeListener('end', onend);
source.removeListener('close', onclose);
source.removeListener('error', onerror);
dest.removeListener('error', onerror);
source.removeListener('end', cleanup);
source.removeListener('close', cleanup);
dest.removeListener('close', cleanup);
}
source.on('end', cleanup);
source.on('close', cleanup);
dest.on('close', cleanup);
dest.emit('pipe', source);
// Allow for unix-like usage: A.pipe(B).pipe(C)
return dest;
};`,
}

+ 1
- 3
plugins/script/plugin.go View File

@ -1,8 +1,6 @@
package main // import "code.dopame.me/veonik/squircy3/plugins/script"
import (
"fmt"
"code.dopame.me/veonik/squircy3/config"
"code.dopame.me/veonik/squircy3/plugin"
"code.dopame.me/veonik/squircy3/vm"
@ -15,7 +13,7 @@ import (
const pluginName = "script"
func main() {
fmt.Println(pluginName, "- a plugin for squircy3")
plugin.Main(pluginName)
}
// Initialize is a valid plugin.Initializer


+ 1
- 3
plugins/squircy2_compat/plugin.go View File

@ -1,8 +1,6 @@
package main // import "code.dopame.me/veonik/squircy3/plugins/squircy2_compat"
import (
"fmt"
"code.dopame.me/veonik/squircy3/config"
"code.dopame.me/veonik/squircy3/event"
"code.dopame.me/veonik/squircy3/irc"
@ -16,7 +14,7 @@ import (
const pluginName = "squircy2_compat"
func main() {
fmt.Println(pluginName, "- a plugin for squircy3")
plugin.Main(pluginName)
}
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {


+ 0
- 4
plugins/squircy2_compat/runtime.go View File

@ -65,10 +65,6 @@ func (cb *callback) Handle(ev *event.Event) {
}
func (p *HelperSet) setDispatcher(gr *goja.Runtime) {
//getFnName := func(fn goja.Value) (name string) {
// s := sha256.Sum256([]byte(fmt.Sprintf("%p", fn)))
// return fmt.Sprintf("__Handler%x", s)
//}
if p.funcs != nil {
for _, f := range p.funcs {
p.events.Unbind(f.eventType, f)


+ 1174
- 0
testdata/es6-promise.js
File diff suppressed because it is too large
View File


+ 15
- 0
testdata/package.json View File

@ -0,0 +1,15 @@
{
"private": true,
"dependencies": {
"@babel/standalone": "^7.5.5",
"assert": "^2.0.0",
"assert-polyfill": "^0.0.0",
"buffer": "^5.2.1",
"core-js-bundle": "^3.1.4",
"error-polyfill": "^0.1.2",
"process": "^0.11.10",
"readable-stream": "^3.6.0",
"regenerator-runtime": "^0.13.3",
"regenerator-transform": "^0.14.1"
}
}

+ 4
- 1
vm/require.go View File

@ -146,18 +146,21 @@ func require(runtime *goja.Runtime, parent *Module, stack []string) func(goja.Fu
}
}
// A Module is a javascript module identified by a name and full path.
type Module struct {
Name string
Path string
Main string
Body string
// etag is the sha256 hash of the original file.
etag [sha256.Size]byte
prog *goja.Program
root *Module
registry *Registry
// value is the evaluated value in the currently running VM.
value *goja.Object
}
@ -224,7 +227,7 @@ func (m *Module) requireRelative(name string) (*Module, error) {
}
func (m *Module) FullPath() string {
return filepath.Clean(filepath.Join(m.Path, m.Name))
return filepath.Clean(filepath.Join(m.Path, m.Main))
}
func (m *Module) String() string {


+ 354
- 0
vm/result.go View File

@ -0,0 +1,354 @@
package vm
import (
"fmt"
"math/rand"
"regexp"
"time"
"github.com/dop251/goja"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
var ErrExecutionCancelled = errors.New("execution cancelled")
// A Result is the output from executing synchronous code on a VM.
type Result struct {
// Closed when the result is ready. Read from this channel to detect when
// the result has been populated and is safe to inspect.
Ready chan struct{}
// Error associated with the result, if any. Only read from this after
// the result is ready.
Error error
// Value associated with the result if there is no error. Only read from
// this after the result is ready.
Value goja.Value
// vmdone is a copy of the VM's done channel at the time Run* is called.
// This removes the need to synchronize when reading from the channel
// since the copy is made while the VM is locked.
vmdone chan struct{}
// cancel is closed to signal that the result is no longer needed.
cancel chan struct{}
}
// resolve populates the result with the given value or error and signals ready.
func newResult(vmdone chan struct{}) *Result {
r := &Result{Ready: make(chan struct{}), cancel: make(chan struct{}), vmdone: vmdone}
go func() {
for {
select {
case <-r.Ready:
// close the cancel channel if we need to
r.Cancel()
return
case <-r.cancel:
// signal to cancel received, resolve with an error
r.resolve(nil, ErrExecutionCancelled)
case <-r.vmdone:
// VM shutdown without resolving, cancel execution
r.Cancel()
}
}
}()
return r
}
// resolve populates the result with the given value or error and signals ready.
func (r *Result) resolve(v goja.Value, err error) {
select {
case <-r.Ready:
fmt.Println("resolve called on already finished Result")
default:
r.Error = err
r.Value = v
close(r.Ready)
}
}
// Await blocks until the result is ready and returns the result or error.
func (r *Result) Await() (goja.Value, error) {
<-r.Ready
return r.Value, r.Error
}
// Cancel the result to halt execution.
func (r *Result) Cancel() {
select {
case <-r.cancel:
// already cancelled, don't bother
default:
close(r.cancel)
}
}
// runFunc is a proxy for the Do method on a VM.
type runFunc func(func(*goja.Runtime))
// AsyncResult handles invocations of asynchronous code that returns promises.
// An AsyncResult accepts any goja.Value; non-promises are supported so this
// is safe (if maybe a bit inefficient) to wrap all results produced by using
// one of the Run* methods on a VM.
type AsyncResult struct {
// Closed when the result is ready. Read from this channel to detect when
// the result has been populated and is safe to inspect.
Ready chan struct{}
// Error associated with the result, if any. Only read from this after
// the result is ready.
Error error
// Value associated with the result if there is no error. Only read from
// this after the result is ready.
Value goja.Value
// syncResult contains the original synchronous result.
// Its value may contain a Promise although other types are also handled.
syncResult *Result
// Unique javascript variable containing the result.
stateVar string
// vmdone is a copy of the VM's done channel at the time Run* is called.
// This removes the need to synchronize when reading from the channel
// since the copy is made while the VM is locked.
vmdone chan struct{}
// vmdo will be a pointer to the run method on a scheduler.
// This isn't strictly necessary (a pointer to VM would be fine) but
// this forced indirection reduces the chance of a Result trying to do
// something it shouldn't.
vmdo runFunc
// cancel is closed to signal that the result is no longer needed.
cancel chan struct{}
// waiting is initialized in the run method and used to synchronize
// the result-ready check.
waiting chan struct{}
// done is closed when the run method returns.
// The goroutine spawned by newAsyncResult waits to return until this
// channel is closed.
done chan struct{}
}
func uniqueResultIdentifier() string {
const chars = "abcdefghijklmnopqrstuvwxyzABZDEFGHIJKLMNOPQRSTUVWXYZ12345678890"
b := make([]byte, 20)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
}
return "____squircy3_await_" + string(b)
}
func newAsyncResult(sr *Result, vmdone chan struct{}, vmdo runFunc) *AsyncResult {
r := &AsyncResult{
Ready: make(chan struct{}),
syncResult: sr,
stateVar: uniqueResultIdentifier(),
vmdo: vmdo,
vmdone: vmdone,
cancel: make(chan struct{}),
done: make(chan struct{}),
}
go func() {
// wait until the original Result is ready
<-sr.Ready
if sr.Error != nil {
// already have an error, resolve immediately
r.resolve(sr.Value, sr.Error)
return
}
// schedule the job to resolve the value
r.vmdo(r.run)
// note that the scheduler may have been stopped since the
// original Result was ready.
// block until the result is cancelled, the VM is shut down,
// or the result is ready.
select {
case <-r.cancel:
r.resolve(nil, ErrExecutionCancelled)
return
case <-r.vmdone:
r.resolve(nil, ErrExecutionCancelled)
return
case <-r.done:
// carry on
}
}()
return r
}
// resolve populates the result with the given value or error and signals ready.
func (r *AsyncResult) resolve(v goja.Value, err error) {
select {
case <-r.Ready:
logrus.Warnln("resolve called on already finished Result")
default:
r.Error = err
r.Value = v
close(r.Ready)
r.vmdo(r.cleanup)
}
}
// Await blocks until the result is ready and returns the result or error.
func (r *AsyncResult) Await() (goja.Value, error) {
<-r.Ready
return r.Value, r.Error
}
// Cancel the result to halt execution.
func (r *AsyncResult) Cancel() {
select {
case <-r.cancel:
// already cancelled, don't bother
default:
close(r.cancel)
}
}
func getOrCreateResultHandler(gr *goja.Runtime) goja.Callable {
if v := gr.Get("_squircy_handle_result"); v != nil {
fn, _ := goja.AssertFunction(v)
return fn
}
// this script handles the resolution of the Promise if the value is a
// Promise, sets the error if the value is an Error, or sets the result
// as the value for anything else.
v, err := gr.RunString(`
var handler = function(state) {
if(typeof Promise !== 'undefined' && state.value instanceof Promise) {
state.value
.then(function(result) { state.result = result; })
.catch(function(error) { state.error = error; })
.finally(function() { state.done = true; });
} else if(state.error instanceof Error) {
state.error = state.value;
state.done = true;
} else {
state.result = state.value;
state.done = true;
}
}
handler;`)
if err != nil {
logrus.Warnln("unable to set async result handler:", err.Error())
return nil
}
gr.Set("____squircy3_handle_result", v)
fn, _ := goja.AssertFunction(v)
return fn
}
func (r *AsyncResult) run(gr *goja.Runtime) {
defer func() {
select {
case <-r.done:
// already closed
default:
close(r.done)
}
}()
o := gr.NewObject()
_ = o.Set("value", r.syncResult.Value)
_ = o.Set("result", goja.Undefined())
_ = o.Set("error", goja.Undefined())
_ = o.Set("done", false)
gr.Set(r.stateVar, o)
hr := getOrCreateResultHandler(gr)
if hr == nil {
r.resolve(nil, errors.New("unable to get result handler"))
return
}
_, err := hr(nil, gr.Get(r.stateVar))
if err != nil {
r.resolve(nil, err)
return
}
go r.loop()
}
func (r *AsyncResult) cleanup(gr *goja.Runtime) {
gr.Set(r.stateVar, goja.Undefined())
}
func (r *AsyncResult) loop() {
defer func() {
select {
case <-r.Ready:
// already closed, no need to close it again
return
default:
close(r.Ready)
}
}()
// delay is how long to wait until we check for a result
delay := 10 * time.Microsecond
for {
if delay < 100*time.Millisecond {
// backoff sharply at first but stop at 100ms between checks
delay = delay * 10
}
select {
case <-r.cancel:
r.Error = errors.New("cancelled")
return
case <-r.vmdone:
// VM shutdown without resolving, cancel execution
r.Cancel()
continue
case <-time.After(delay):
r.waiting = make(chan struct{})