Skip to content

Commit

Permalink
feat: Merging 1.1.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
krotik committed Dec 13, 2020
1 parent c460a11 commit 30700c8
Show file tree
Hide file tree
Showing 22 changed files with 505 additions and 56 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [1.1.0](https://devt.de///compare/v1.0.4...v1.1.0) (2020-12-13)


### Features

* Adding plugin support to ECAL ([56be402](https://devt.de///commit/56be402e464b5f9574295e717a4cebc382852c26))


### Bug Fixes

* Pack can now run under Windows / adjusted example scripts ([6f24339](https://devt.de///commit/6f243399ea5c3042ad1092b94a6372e0fd55a5e0))

### [1.0.4](https://devt.de///compare/v1.0.3...v1.0.4) (2020-12-07)

### [1.0.3](https://devt.de///compare/v1.0.2...v1.0.3) (2020-12-07)
Expand Down
13 changes: 8 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export NAME=ecal
export TAG=`git describe --abbrev=0 --tags`
export CGO_ENABLED=0

# CGO_ENABLED is enabled here to support Go plugins
# if Go plugins are not used this can be disabled.
export CGO_ENABLED=1
export GOOS=linux

all: build
Expand Down Expand Up @@ -29,16 +32,16 @@ build: clean mod generate fmt vet
go build -ldflags "-s -w" -o $(NAME) cli/*.go

build-mac: clean mod generate fmt vet
GOOS=darwin GOARCH=amd64 go build -o $(NAME).mac cli/*.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $(NAME).mac cli/*.go

build-win: clean mod generate fmt vet
GOOS=windows GOARCH=amd64 go build -o $(NAME).exe cli/*.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $(NAME).exe cli/*.go

build-arm7: clean mod generate fmt vet
GOOS=linux GOARCH=arm GOARM=7 go build -o $(NAME).arm7 cli/*.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o $(NAME).arm7 cli/*.go

build-arm8: clean mod generate fmt vet
GOOS=linux GOARCH=arm64 go build -o $(NAME).arm8 cli/*.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(NAME).arm8 cli/*.go

dist: build build-win build-mac build-arm7 build-arm8
rm -fR dist
Expand Down
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ ECAL is an ECA (Event Condition Action) language for concurrent event processing

Features
--------
- Simple intuitive syntax
- Minimalistic base language (by default only writing to a log is supported)
- Language can be easily extended either by auto generating bridge adapters to Go functions or by adding custom function into the stdlib
- Simple intuitive syntax.
- Minimalistic base language (by default only writing to a log is supported).
- Language can be easily extended either by auto generating bridge adapters to Go functions or by adding custom function into the standard library (stdlib).
- External events can be easily pushed into the interpreter and scripts written in ECAL can react to these events.
- Simple but powerful concurrent event-based processing supporting priorities and scoping for control flow.
- Handling event rules can match on event state and rules can suppress each other.
Expand Down Expand Up @@ -127,6 +127,33 @@ All errors are collected in the returned monitor.
monitor.RootMonitor().AllErrors()
```

### Using Go plugins in ECAL

ECAL supports to extend the standard library (stdlib) functions via [Go plugins](https://golang.org/pkg/plugin/). The intention of this feature is to allow easy expansion of the standard library even with platform dependent code.

Go plugins come with quite a few extra [requirements and drawbacks](https://www.reddit.com/r/golang/comments/b6h8qq/is_anyone_actually_using_go_plugins/) and should be considered carefully. One major requirement is that `CGO_ENABLED` must be enabled because plugins use the libc dynamic linker. Using [CGO](https://blog.golang.org/cgo) means that cross-platform compilation is difficult as the compilation requires platform specific system libraries.

ECAL stdlib functions defined in plugins must conform to the following interface:
```
/*
ECALPluginFunction models a callable function in ECAL which can be imported via a plugin.
*/
type ECALPluginFunction interface {
/*
Run executes this function with a given list of arguments.
*/
Run(args []interface{}) (interface{}, error)
/*
DocString returns a descriptive text about this function.
*/
DocString() string
}
```

There is a plugin example in the directory `examples/plugin`. The example assumes that the interpreter binary has been compiled with `CGO_ENABLED` which is the default when building the interpreter via the Makefile but not when using the pre-compiled binaries except the Linux binary. The plugin .so file can be compiled with `buildplugin.sh` (the Go compiler must have the same version as the one which compiled the interpreter binary). Running the example with `run.sh` will make the ECAL interpreter load the compiled plugin before executing the ECAL code. The example demonstrates normal and error output. The plugins to load can be defined in a `.ecal.json` file in the interpreter's root directory.

### Further Reading:

- [ECA Language](ecal.md)
Expand Down
120 changes: 83 additions & 37 deletions cli/tool/interpret.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
package tool

import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"

"devt.de/krotik/common/fileutil"
Expand Down Expand Up @@ -54,7 +56,8 @@ type CLIInterpreter struct {
CustomWelcomeMessage string
CustomHelpString string

EntryFile string // Entry file for the program
EntryFile string // Entry file for the program
LoadPlugins bool // Flag if stdlib plugins should be loaded

// Parameter these can either be set programmatically or via CLI args

Expand All @@ -75,7 +78,8 @@ type CLIInterpreter struct {
NewCLIInterpreter creates a new commandline interpreter for ECAL.
*/
func NewCLIInterpreter() *CLIInterpreter {
return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil, nil, os.Stdout}
return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "",
true, nil, nil, nil, nil, os.Stdout}
}

/*
Expand Down Expand Up @@ -225,63 +229,105 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
return nil
}

err := i.CreateTerm()
err := i.LoadStdlibPlugins(interactive)

if interactive {
fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
}
if err == nil {
err = i.CreateTerm()

// Create Runtime Provider
if interactive {
fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
}

if err == nil {
// Create Runtime Provider

if err = i.CreateRuntimeProvider("console"); err == nil {
if err == nil {

tid := i.RuntimeProvider.NewThreadID()
if err = i.CreateRuntimeProvider("console"); err == nil {

if interactive {
if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
fmt.Fprint(i.LogOut, fmt.Sprintf("Log level: %v - ", lll.Level()))
}
tid := i.RuntimeProvider.NewThreadID()

fmt.Fprintln(i.LogOut, fmt.Sprintf("Root directory: %v", *i.Dir))
if interactive {
if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
fmt.Fprint(i.LogOut, fmt.Sprintf("Log level: %v - ", lll.Level()))
}

if i.CustomWelcomeMessage != "" {
fmt.Fprintln(i.LogOut, fmt.Sprintf(i.CustomWelcomeMessage))
fmt.Fprintln(i.LogOut, fmt.Sprintf("Root directory: %v", *i.Dir))

if i.CustomWelcomeMessage != "" {
fmt.Fprintln(i.LogOut, fmt.Sprintf(i.CustomWelcomeMessage))
}
}
}

// Execute file if given
// Execute file if given

if err = i.LoadInitialFile(tid); err == nil {
if err = i.LoadInitialFile(tid); err == nil {

// Drop into interactive shell
// Drop into interactive shell

if interactive {
if interactive {

// Add history functionality without file persistence
// Add history functionality without file persistence

i.Term, err = termutil.AddHistoryMixin(i.Term, "",
func(s string) bool {
return i.isExitLine(s)
})
i.Term, err = termutil.AddHistoryMixin(i.Term, "",
func(s string) bool {
return i.isExitLine(s)
})

if err == nil {
if err == nil {

if err = i.Term.StartTerm(); err == nil {
var line string
if err = i.Term.StartTerm(); err == nil {
var line string

defer i.Term.StopTerm()
defer i.Term.StopTerm()

fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")

line, err = i.Term.NextLine()
for err == nil && !i.isExitLine(line) {
trimmedLine := strings.TrimSpace(line)
line, err = i.Term.NextLine()
for err == nil && !i.isExitLine(line) {
trimmedLine := strings.TrimSpace(line)

i.HandleInput(i.Term, trimmedLine, tid)
i.HandleInput(i.Term, trimmedLine, tid)

line, err = i.Term.NextLine()
line, err = i.Term.NextLine()
}
}
}
}
}
}
}
}

return err
}

/*
LoadStdlibPlugins load plugins from .ecal.json.
*/
func (i *CLIInterpreter) LoadStdlibPlugins(interactive bool) error {
var err error

if i.LoadPlugins {
confFile := filepath.Join(*i.Dir, ".ecal.json")
if ok, _ := fileutil.PathExists(confFile); ok {

if interactive {
fmt.Fprintln(i.LogOut, fmt.Sprintf("Loading stdlib plugins from %v", confFile))
}

var content []byte
if content, err = ioutil.ReadFile(confFile); err == nil {
var conf map[string]interface{}
if err = json.Unmarshal(content, &conf); err == nil {
if stdlibPlugins, ok := conf["stdlibPlugins"]; ok {
err = fmt.Errorf("Config stdlibPlugins should be a list")
if plugins, ok := stdlibPlugins.([]interface{}); ok {
err = nil
if errs := stdlib.LoadStdlibPlugins(plugins); len(errs) > 0 {
for _, e := range errs {
fmt.Fprintln(i.LogOut, fmt.Sprintf("Error loading plugins: %v", e))
}
err = fmt.Errorf("Could not load plugins defined in .ecal.json")
}
}
}
Expand Down
38 changes: 35 additions & 3 deletions cli/tool/interpret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ func newTestInterpreter() *CLIInterpreter {
func newTestInterpreterWithConfig() *CLIInterpreter {
tin := newTestInterpreter()

// Setup
if res, _ := fileutil.PathExists(testDir); res {
os.RemoveAll(testDir)
}
Expand All @@ -68,8 +67,6 @@ func newTestInterpreterWithConfig() *CLIInterpreter {

tin.CustomWelcomeMessage = "123"

// Teardown

return tin
}

Expand Down Expand Up @@ -144,6 +141,41 @@ func TestInterpretBasicFunctions(t *testing.T) {
t.Error("Unexpected entryfile:", tin.EntryFile)
return
}

flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing

osArgs = []string{"foo", "bar"}

// Try to load non-existing plugins (success case is tested in stdlib)

tin = newTestInterpreterWithConfig()
defer tearDown()

l1 := ""
tin.LogFile = &l1
l2 := ""
tin.LogLevel = &l2

ioutil.WriteFile(filepath.Join(testDir, ".ecal.json"), []byte(`{
"stdlibPlugins" : [{
"package" : "mypkg",
"name" : "myfunc",
"path" : "./myfunc.so",
"symbol" : "ECALmyfunc"
}]
}`), 0666)

err := tin.Interpret(true)

if err == nil || err.Error() != "Could not load plugins defined in .ecal.json" {
t.Error("Unexpected result:", err.Error())
return
}

if !strings.Contains(testLogOut.String(), "Error loading plugins") {
t.Error("Unexpected result:", testLogOut.String())
return
}
}

func TestCreateRuntimeProvider(t *testing.T) {
Expand Down
11 changes: 7 additions & 4 deletions cli/tool/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"unicode"
Expand Down Expand Up @@ -175,18 +176,20 @@ func (p *CLIPacker) packFiles(w *zip.Writer, filePath string, zipPath string) er
for _, file := range files {
if !file.IsDir() {
var data []byte
if data, err = ioutil.ReadFile(filepath.Join(filePath, file.Name())); err == nil {
diskfile := filepath.Join(filePath, file.Name())
if data, err = ioutil.ReadFile(diskfile); err == nil {
var f io.Writer
if f, err = w.Create(filepath.Join(zipPath, file.Name())); err == nil {
if f, err = w.Create(path.Join(zipPath, file.Name())); err == nil {
if bytes, err = f.Write(data); err == nil {
fmt.Fprintln(p.LogOut, fmt.Sprintf("Writing %v bytes for %v",
bytes, filepath.Join(filePath, file.Name())))
bytes, diskfile))
}
}
}
} else if file.IsDir() {
// Path separator in zipfile is always '/'
p.packFiles(w, filepath.Join(filePath, file.Name()),
filepath.Join(zipPath, file.Name()))
path.Join(zipPath, file.Name()))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
/*
ProductVersion is the current version of ECAL
*/
const ProductVersion = "1.0.4"
const ProductVersion = "1.1.0"

/*
Known configuration options for ECAL
Expand Down
Binary file modified examples/embedding/embedding
Binary file not shown.
5 changes: 5 additions & 0 deletions examples/fib/pack.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@echo off
mkdir pack
copy fib.ecal pack
robocopy lib pack\lib
..\..\ecal.exe pack -dir pack -target out.exe fib.ecal
2 changes: 2 additions & 0 deletions examples/fib/run.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
..\..\ecal.exe run fib.ecal
2 changes: 2 additions & 0 deletions examples/game_of_life/run.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
..\..\ecal.exe run game_of_life.ecal
8 changes: 8 additions & 0 deletions examples/plugin/.ecal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"stdlibPlugins" : [{
"package" : "mypkg",
"name" : "myfunc",
"path" : "./myfunc.so",
"symbol" : "ECALmyfunc"
}]
}
2 changes: 2 additions & 0 deletions examples/plugin/buildplugin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
go build -ldflags "-s -w" -buildmode=plugin -o myfunc.so myfunc.go
Loading

0 comments on commit 30700c8

Please sign in to comment.