mirror of https://github.com/veonik/squircy3
Resolve Promises with AsyncResult, work on net node_compat
parent
89fac8b64c
commit
e9caee8cb4
|
@ -1,3 +1,4 @@
|
|||
out/*
|
||||
node_modules/*
|
||||
testdata/node_modules/*
|
||||
Dockerfile
|
|
@ -3,5 +3,8 @@
|
|||
!.dockerignore
|
||||
out/*
|
||||
node_modules/*
|
||||
testdata/node_modules/*
|
||||
testdata/yarn-error.log
|
||||
testdata/yarn.lock
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
|
9
Makefile
9
Makefile
|
@ -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
go.mod
1
go.mod
|
@ -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
go.sum
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package transformer // import "code.dopame.me/veonik/squircy3/plugins/babel/transformer"
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
|
@ -11,7 +11,7 @@ type Babel struct {
|
|||
transform goja.Callable
|
||||
}
|
||||
|
||||
func NewBabel(r *goja.Runtime) (*Babel, error) {
|
||||
func New(r *goja.Runtime) (*Babel, error) {
|
||||
b := &Babel{runtime: r}
|
||||
v, err := b.runtime.RunString(`
|
||||
this.global = this.global || this;
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};`,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;`,
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
`,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {}
|
||||
`,
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,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,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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||