fix: Re-initializing after I destroyed the original repository.

This commit is contained in:
James Wells 2021-02-25 17:46:40 -08:00
parent 5863999e8c
commit 8b54fc32c5
Signed by: jwells
GPG key ID: 73196D10B8E65666
20 changed files with 1359 additions and 0 deletions

44
.drone.yml Normal file
View file

@ -0,0 +1,44 @@
---
kind: pipeline
type: docker
name: default
platform:
os: linux
arch: arm
trigger:
branch:
include:
- development
exclude:
- main
clone:
depth: 1
volumes:
- name: dockersock
host:
path: /run/docker.sock
image_pull_secrets:
- docker
steps:
- name: Build ${DRONE_REPO} Container Image
image: plugins/docker
volumes:
- name: dockersock
path: /var/run/docker.sock
settings:
auth:
from_secret: Docker
auto_tag: true
dockerfile: docker/Dockerfile
# mirror: "https://registry.dragonheim.net:443"
username:
from_secret: docker_username
repo: ${DRONE_REPO}
# target: development

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# ---> Go
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
bin/
vendor/
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Ignore various IDE
.idea
*.iml
*.ipr
.vscode
debug
# Ignore various temporary files
*.swp
*.tmp
*.bak
*.log
*.pid
# Ignore G'Agent configuration file.
gagent.hcl

4
CHANGELOG.md Normal file
View file

@ -0,0 +1,4 @@
### [0.0.0](https://git.dragonheim.net/dragonheim/gagent/commit/5863999) (20210425)
---
#### Features
* [Initial Commit](https://git.dragonheim.net/dragonheim/gagent/commit/5863999) -- James Wells

8
LANGUAGE.md Normal file
View file

@ -0,0 +1,8 @@
# G'Agent
## AgentTCL Language Extension
### TODO
Everything. :(
## G'Agent Language Extension
### TODO
Everything. :(

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
MIT License
Copyright (c) 2017-2021 James Wells <jwells@dragonheim.net>
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.

20
docker/Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM golang:1.16-alpine3.12 as builder
WORKDIR /gagent
COPY . .
RUN apk add --no-cache zeromq-dev build-base git && \
go build -o /gagent/bin/gagent gagent/main.go && \
strip /gagent/bin/gagent
FROM alpine:3.12
LABEL Name="G'Agent" Maintainer="jwells@dragonheim.net" License="MIT License"
RUN apk add --no-cache zeromq; mkdir -p -m 0700 /etc/gagent
COPY --from=builder /gagent/examples/example_gagent.hcl /etc/gagent/gagent.hcl
COPY --from=builder /gagent/bin/gagent /usr/bin/
EXPOSE 35570/tcp
VOLUME /etc/gagent
CMD ["/usr/bin/gagent"]

8
examples/add-two.tcl Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env tcl
#
#
set val1 1
set val2 2
set result [expr {val1 + val2}]
puts result

View file

@ -0,0 +1,83 @@
/*
* This is the name of this node and is only used
* for logging purposes.
*
* Optional.
*/
name = "gagent-zulu.example.org"
/*
* This is the mode that this node operates in. There
* are three modes;
* client == Clients read the local agent file and
* forwards the contents on to a router
*
* router == Routers accept agents from clients and
* other routers and accepts responses to
* agents from workers and other routers.
*
* worker == Workers collect and process agents and
* send responses to routers for return
* the requesting client.
*
* Required.
*/
mode = "router"
/*
* This is the UUID used throughout the G'Agent system
* to uniquely identify this node.
*
* Required.
*/
// uuid = "04f97538-270d-4ca3-b782-e09ef35830e9"
/*
* This is a list of known G'Agent routers. At least
* one router is required for workers and clients. If
* there is more than one router, clients and workers
* will connect to them in sequential order.
*/
// router "alpha" {
// routerid = "04f97538-270d-4cb3-b782-e09ef35830e9"
// address = "gagent-alpha.example.org"
// tags = [ "a", "b", "c", "d" ]
// }
//
// router "beta" {
// routerid = "04f97538-270d-4cc3-b782-e09ef35830e9"
// address = "gagent-beta.example.org"
// tags = [ "a", "c", "e", "g" ]
// }
//
// router "charlie" {
// routerid = "04f97538-270d-4cd3-b782-e09ef35830e9"
// address = "gagent-charlie.example.org"
// tags = [ "b", "d", "f", "h" ]
// }
/*
* This is a list of known G'Agent workers. This is only
* used by routers to determine which workers are
* allowed to accept and respond to agents.
*
* At least one worker is reuqired for routers.
*/
// worker "alpha" {
// workerid = "04f97538-270d-4ce3-b782-e09ef35830e9"
// address = "gagent-alpha.example.org"
// tags = [ "a", "b", "c", "d" ]
// }
//
// worker "beta" {
// workerid = "04f97538-270d-4cf3-b782-e09ef35830e9"
// address = "gagent-beta.example.org"
// tags = [ "a", "c", "e", "g" ]
// }
//
// worker "charlie" {
// workerid = "04f97538-270d-4c04-b782-e09ef35830e9"
// address = "gagent-charlie.example.org"
// tags = [ "b", "d", "f", "h" ]
// }

4
examples/hello-world.tcl Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env tcl
#
#
puts {hello, world}

BIN
gagent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

170
gagent/main.go Normal file
View file

@ -0,0 +1,170 @@
package main
import (
"fmt"
"io/ioutil"
"os"
// "math/rand"
// "time"
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
// client "git.dragonheim.net/dragonheim/gagent/src/client"
// router "git.dragonheim.net/dragonheim/gagent/src/router"
// worker "git.dragonheim.net/dragonheim/gagent/src/worker"
docopt "github.com/aviddiviner/docopt-go"
hclsimple "github.com/hashicorp/hcl/v2/hclsimple"
uuid "github.com/nu7hatch/gouuid"
)
var exitCodes = struct {
m map[string]int
}{m: map[string]int{
"SUCCESS": 0,
"CONFIG_FILE_MISSING": 1,
"SETUP_FAILED": 2,
"INVALID_MODE": 3,
"AGENT_LOAD_FAILED": 4,
}}
func main() {
var config gs.GagentConfig
var configFile string = "/etc/gagent/gagent.hcl"
config.Name, _ = os.Hostname()
/*
* Set a default UUID for this node.
* This is used throughout the G'Agent system to uniquely identify this node.
* It can be overriden in the configuration file by setting uuid
*/
// rand.Seed(time.Now().UnixNano())
identity, _ := uuid.NewV5(uuid.NamespaceURL, []byte("gagent"+config.Name))
config.UUID = identity.String()
/*
* By default, we want to listen on all IP addresses. It can be overriden
* in the configuration file by setting listenaddr
*/
config.ListenAddr = "0.0.0.0"
/*
* By default, G'Agent will use port 35570 to communicate with the routers,
* but you can override it by setting the listenport in the configuration
* file
*/
config.ListenPort = 35570
/*
* Create a usage variable and then use that to declare the arguments and
* options. This allows us to use DocOpt for consistency between usage help
* and available arguments / options. Documentation is available at;
* http://docopt.org/
*/
usage := "G'Agents \n"
usage += "\n"
usage += " Go based mobile agent system, loosely inspired by the Agent Tcl / D'Agents \n"
usage += " system created by Robert S. Gray of Dartmouth college. \n"
usage += "\n"
usage += "Usage: \n"
usage += " gagent [--config=<config>] [--agent=<file>] \n"
usage += " gagent setup [--config=<config>] \n"
usage += "\n"
usage += "Arguments: \n"
usage += " client -- Start as a G'Agent client \n"
usage += " <file> -- filename of the agent to be uploaded to the G'Agent network \n"
usage += "\n"
usage += " setup -- Write inital configuration file \n"
usage += "\n"
usage += "Options:\n"
usage += " config=<config> [default: /etc/gagent/gagent.hcl] \n"
/*
* Consume the usage variable and the command line arguments to create a
* dictionary of the command line arguments.
*/
arguments, _ := docopt.ParseDoc(usage)
fmt.Printf("Arguments are %v\n", arguments)
if arguments["--config"] != nil {
configFile = arguments["--config"].(string)
}
/*
* Let the command line mode override the configuration.
*/
if arguments["setup"] == true {
config.Mode = "setup"
} else {
err := hclsimple.DecodeFile(configFile, nil, &config)
if err != nil {
fmt.Printf("Failed to load configuration file: %s.\n", configFile)
fmt.Println(err)
os.Exit(exitCodes.m["CONFIG_FILE_MISSING"])
}
}
fmt.Printf("Configuration is %v\n", config)
switch config.Mode {
case "client":
/*
* Client mode will send an agent file to a router for processing
* Clients do not process the agent files, only send them as
* requests to a router. If started without arguments, the client
* will contact the router and attempt to retrieve the results
* of it's most recent request.
*/
fmt.Printf("Running in client mode\n")
agent, err := ioutil.ReadFile(arguments["--agent"].(string))
if err == nil {
fmt.Printf("Agent containts %v\n", string(agent))
// fmt.Printf("Forking...\n")
// go client.Main(config.Client, string(agent))
// fmt.Printf("Forked thread has completed\n")
// time.Sleep(10 * time.Second)
} else {
fmt.Printf("Failed to load Agent file: %s.\n", arguments["--agent"].(string))
os.Exit(exitCodes.m["AGENT_LOAD_FAILED"])
}
case "router":
/*
* The 'router' processes routing requests from the agent. The router does
* not handle any of the agent activities beyond processing the agent's
* list of tags and passing the agent and it's storage to either a member
* or client node. Tags are used by the agent to give hints as to where
* it should be routed.
*/
fmt.Printf("Running in router mode\n")
// go router.Main(config.Router)
// select {}
case "worker":
/*
* The 'worker' processes the agent code. The worker nodes do not know
* anything about the network structure. Instead they know only to which
* router(s) they are connected. The worker will execute the agent code and
* pass the agent and it's results to a router.
*/
fmt.Printf("Running in worker mode\n")
// go worker.Main(config.Worker)
// select {}
case "setup":
fmt.Printf("Running in setup mode\n")
os.Exit(exitCodes.m["SETUP_FAILED"])
default:
fmt.Printf("Unknown operating mode, exiting.\n")
os.Exit(exitCodes.m["INVALID_MODE"])
}
os.Exit(exitCodes.m["SUCCESS"])
}

10
go.mod Normal file
View file

@ -0,0 +1,10 @@
module git.dragonheim.net/dragonheim/gagent
go 1.16
require (
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a
github.com/hashicorp/hcl/v2 v2.8.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/pebbe/zmq4 v1.2.2
)

53
go.sum Normal file
View file

@ -0,0 +1,53 @@
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a h1:YJuVATwP+Gzk7nys0U/DKjKkoYp1n/sYm0yi5vX8W8M=
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a/go.mod h1:pBRbUcGboHT5qBceq2Cg/WIcDbO78a8wPxTg8zmS3Hs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hashicorp/hcl/v2 v2.8.2 h1:wmFle3D1vu0okesm8BTLVDyJ6/OL9DCLUwn0b2OptiY=
github.com/hashicorp/hcl/v2 v2.8.2/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pebbe/zmq4 v1.2.2 h1:RZ5Ogp0D5S6u+tSxopnI3afAf0ifWbvQOAw9HxXvZP4=
github.com/pebbe/zmq4 v1.2.2/go.mod h1:7N4y5R18zBiu3l0vajMUWQgZyjv464prE8RCyBcmnZM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

43
src/client/client.go Normal file
View file

@ -0,0 +1,43 @@
package client
import (
"fmt"
"sync"
"time"
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
zmq "github.com/pebbe/zmq4"
)
// Main is the initiation function for a Client
func Main(config gs.GagentConfig, agent string) {
var mu sync.Mutex
fmt.Printf("Did we make it this far?\n")
fmt.Printf("--|%#v|--\n", agent)
connectString := fmt.Sprintf("tcp://%s", config.Routers[0].RouterAddr)
sock, _ := zmq.NewSocket(zmq.DEALER)
defer sock.Close()
sock.SetIdentity(config.UUID)
sock.Connect(connectString)
go func() {
mu.Lock()
sock.SendMessage(agent)
mu.Unlock()
}()
for {
time.Sleep(10 * time.Millisecond)
mu.Lock()
msg, err := sock.RecvMessage(zmq.DONTWAIT)
if err == nil {
fmt.Println(msg[0], config.UUID)
}
mu.Unlock()
}
}

87
src/gstructs/gstructs.go Normal file
View file

@ -0,0 +1,87 @@
package gstructs
// GagentConfig is the primary construct used by all modes
type GagentConfig struct {
Name string `hcl:"name,optional"`
Mode string `hcl:"mode,attr"`
UUID string `hcl:"uuid,optional"`
ListenAddr string `hc:"listenaddr,optional"`
ListenPort int `hc:"listenport,optional"`
Clients []*ClientDetails `hcl:"client,block"`
Routers []*RouterDetails `hcl:"router,block"`
Workers []*WorkerDetails `hcl:"worker,block"`
}
// ClientDetails is details about known clients
type ClientDetails struct {
/*
* Client name for display purposes in logs and
* diagnostics.
*/
ClientName string `hcl:",label"`
/*
* UUID String for the client node. This is used by
* the router to determine which MQ client to send
* the agent's results to. This attempts to keep the
* clients unique globally.
*/
ClientID string `hcl:"clientid,attr"`
}
// RouterDetails is details about known routers
type RouterDetails struct {
/*
* Router name for display purposes in logs and
* diagnostics
*/
RouterName string `hcl:",label"`
/*
* UUID String for the router node. This is used by
* the clients, routers, and workers to determine
* which MQ router to send the agent's requests to.
* This attempts to keep the routers unique globally.
*/
RouterID string `hcl:"routerid,attr"`
/*
* This is the IP Address and port that the router
* will listen on. The router will start up a 0MQ
* service that clients and workers will connect to.
*/
RouterAddr string `hcl:"address,attr"`
/*
* These tags will be passed to the router upon
* connection. The router will then use these
* tags to help determine which worker / client
* to send the client's requests and results to.
*/
RouterTags []string `hcl:"tags,optional"`
}
// WorkerDetails is details about known workers
type WorkerDetails struct {
/*
* Router name for display purposes in logs and
* diagnostics
*/
WorkerName string `hcl:",label"`
/*
* UUID String for the worker node. This is used
* by the router to determine which MQ client to
* send agents to. This attempts to keep the
* workers unique globally.
*/
WorkerID string `hcl:"workerid,attr"`
/*
* These tags will be passed to the router upon
* connection. The router will then use these
* tags to help determine which worker / client
* to send the agent and it's results to.
*/
WorkerTags []string `hcl:"tags,optional"`
}

214
src/picol/commands.go Normal file
View file

@ -0,0 +1,214 @@
package picol
import (
"fmt"
"strconv"
"strings"
)
func ArityErr(i *Interp, name string, argv []string) error {
return fmt.Errorf("Wrong number of args for %s %s", name, argv)
}
func CommandMath(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 3 {
return "", ArityErr(i, argv[0], argv)
}
a, _ := strconv.Atoi(argv[1])
b, _ := strconv.Atoi(argv[2])
var c int
switch {
case argv[0] == "+":
c = a + b
case argv[0] == "-":
c = a - b
case argv[0] == "*":
c = a * b
case argv[0] == "/":
c = a / b
case argv[0] == ">":
if a > b {
c = 1
}
case argv[0] == ">=":
if a >= b {
c = 1
}
case argv[0] == "<":
if a < b {
c = 1
}
case argv[0] == "<=":
if a <= b {
c = 1
}
case argv[0] == "==":
if a == b {
c = 1
}
case argv[0] == "!=":
if a != b {
c = 1
}
default: // FIXME I hate warnings
c = 0
}
return fmt.Sprintf("%d", c), nil
}
func CommandSet(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 3 {
return "", ArityErr(i, argv[0], argv)
}
i.SetVar(argv[1], argv[2])
return argv[2], nil
}
func CommandUnset(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 2 {
return "", ArityErr(i, argv[0], argv)
}
i.UnsetVar(argv[1])
return "", nil
}
func CommandIf(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 3 && len(argv) != 5 {
return "", ArityErr(i, argv[0], argv)
}
result, err := i.Eval(argv[1])
if err != nil {
return "", err
}
if r, _ := strconv.Atoi(result); r != 0 {
return i.Eval(argv[2])
} else if len(argv) == 5 {
return i.Eval(argv[4])
}
return result, nil
}
func CommandWhile(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 3 {
return "", ArityErr(i, argv[0], argv)
}
for {
result, err := i.Eval(argv[1])
if err != nil {
return "", err
}
if r, _ := strconv.Atoi(result); r != 0 {
result, err := i.Eval(argv[2])
switch err {
case PICOL_CONTINUE, nil:
//pass
case PICOL_BREAK:
return result, nil
default:
return result, err
}
} else {
return result, nil
}
}
}
func CommandRetCodes(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 1 {
return "", ArityErr(i, argv[0], argv)
}
switch argv[0] {
case "break":
return "", PICOL_BREAK
case "continue":
return "", PICOL_CONTINUE
}
return "", nil
}
func CommandCallProc(i *Interp, argv []string, pd interface{}) (string, error) {
var x []string
if pd, ok := pd.([]string); ok {
x = pd
} else {
return "", nil
}
i.callframe = &CallFrame{vars: make(map[string]Var), parent: i.callframe}
defer func() { i.callframe = i.callframe.parent }() // remove the called proc callframe
arity := 0
for _, arg := range strings.Split(x[0], " ") {
if len(arg) == 0 {
continue
}
arity++
i.SetVar(arg, argv[arity])
}
if arity != len(argv)-1 {
return "", fmt.Errorf("Proc '%s' called with wrong arg num", argv[0])
}
body := x[1]
result, err := i.Eval(body)
if err == PICOL_RETURN {
err = nil
}
return result, err
}
func CommandProc(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 4 {
return "", ArityErr(i, argv[0], argv)
}
return "", i.RegisterCommand(argv[1], CommandCallProc, []string{argv[2], argv[3]})
}
func CommandReturn(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 1 && len(argv) != 2 {
return "", ArityErr(i, argv[0], argv)
}
var r string
if len(argv) == 2 {
r = argv[1]
}
return r, PICOL_RETURN
}
func CommandError(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 1 && len(argv) != 2 {
return "", ArityErr(i, argv[0], argv)
}
return "", fmt.Errorf(argv[1])
}
func CommandPuts(i *Interp, argv []string, pd interface{}) (string, error) {
if len(argv) != 2 {
return "", fmt.Errorf("Wrong number of args for %s %s", argv[0], argv)
}
fmt.Println(argv[1])
return "", nil
}
func (i *Interp) RegisterCoreCommands() {
name := [...]string{"+", "-", "*", "/", ">", ">=", "<", "<=", "==", "!="}
for _, n := range name {
i.RegisterCommand(n, CommandMath, nil)
}
i.RegisterCommand("set", CommandSet, nil)
i.RegisterCommand("unset", CommandUnset, nil)
i.RegisterCommand("if", CommandIf, nil)
i.RegisterCommand("while", CommandWhile, nil)
i.RegisterCommand("break", CommandRetCodes, nil)
i.RegisterCommand("continue", CommandRetCodes, nil)
i.RegisterCommand("proc", CommandProc, nil)
i.RegisterCommand("return", CommandReturn, nil)
i.RegisterCommand("error", CommandError, nil)
i.RegisterCommand("puts", CommandPuts, nil)
}

250
src/picol/parser.go Normal file
View file

@ -0,0 +1,250 @@
package picol
import (
"unicode"
"unicode/utf8"
)
const (
PT_ESC = iota
PT_STR
PT_CMD
PT_VAR
PT_SEP
PT_EOL
PT_EOF
)
type Parser struct {
text string
p, start, end, ln int
insidequote int
Type int
}
func InitParser(text string) *Parser {
return &Parser{text, 0, 0, 0, len(text), 0, PT_EOL}
}
func (p *Parser) next() {
_, w := utf8.DecodeRuneInString(p.text[p.p:])
p.p += w
p.ln -= w
}
func (p *Parser) current() rune {
r, _ := utf8.DecodeRuneInString(p.text[p.p:])
return r
}
func (p *Parser) token() (t string) {
defer recover()
return p.text[p.start:p.end]
}
func (p *Parser) parseSep() string {
p.start = p.p
for ; p.p < len(p.text); p.next() {
if !unicode.IsSpace(p.current()) {
break
}
}
p.end = p.p
p.Type = PT_SEP
return p.token()
}
func (p *Parser) parseEol() string {
p.start = p.p
for ; p.p < len(p.text); p.next() {
if p.current() == ';' || unicode.IsSpace(p.current()) {
// pass
} else {
break
}
}
p.end = p.p
p.Type = PT_EOL
return p.token()
}
func (p *Parser) parseCommand() string {
level, blevel := 1, 0
p.next() // skip
p.start = p.p
Loop:
for {
switch {
case p.ln == 0:
break Loop
case p.current() == '[' && blevel == 0:
level++
case p.current() == ']' && blevel == 0:
level--
if level == 0 {
break Loop
}
case p.current() == '\\':
p.next()
case p.current() == '{':
blevel++
case p.current() == '}' && blevel != 0:
blevel--
}
p.next()
}
p.end = p.p
p.Type = PT_CMD
if p.p < len(p.text) && p.current() == ']' {
p.next()
}
return p.token()
}
func (p *Parser) parseVar() string {
p.next() // skip the $
p.start = p.p
if p.current() == '{' {
p.Type = PT_VAR
return p.parseBrace()
}
for p.p < len(p.text) {
c := p.current()
if unicode.IsLetter(c) || ('0' <= c && c <= '9') || c == '_' {
p.next()
continue
}
break
}
if p.start == p.p { // It's just a single char string "$"
p.start = p.p - 1
p.end = p.p
p.Type = PT_STR
} else {
p.end = p.p
p.Type = PT_VAR
}
return p.token()
}
func (p *Parser) parseBrace() string {
level := 1
p.next() // skip
p.start = p.p
Loop:
for p.p < len(p.text) {
c := p.current()
switch {
case p.ln >= 2 && c == '\\':
p.next()
case p.ln == 0 || c == '}':
level--
if level == 0 || p.ln == 0 {
break Loop
}
case c == '{':
level++
}
p.next()
}
p.end = p.p
if p.ln != 0 { // Skip final closed brace
p.next()
}
return p.token()
}
func (p *Parser) parseString() string {
newword := p.Type == PT_SEP || p.Type == PT_EOL || p.Type == PT_STR
if c := p.current(); newword && c == '{' {
p.Type = PT_STR
return p.parseBrace()
} else if newword && c == '"' {
p.insidequote = 1
p.next() // skip
}
p.start = p.p
Loop:
for ; p.ln != 0; p.next() {
switch p.current() {
case '\\':
if p.ln >= 2 {
p.next()
}
case '$', '[':
break Loop
case '"':
if p.insidequote != 0 {
p.end = p.p
p.Type = PT_ESC
p.next()
p.insidequote = 0
return p.token()
}
}
if p.current() == ';' || unicode.IsSpace(p.current()) {
if p.insidequote == 0 {
break Loop
}
}
}
p.end = p.p
p.Type = PT_ESC
return p.token()
}
func (p *Parser) parseComment() string {
for p.ln != 0 && p.current() != '\n' {
p.next()
}
return p.token()
}
func (p *Parser) GetToken() string {
for {
if p.ln == 0 {
if p.Type != PT_EOL && p.Type != PT_EOF {
p.Type = PT_EOL
} else {
p.Type = PT_EOF
}
return p.token()
}
switch p.current() {
case ' ', '\t', '\r':
if p.insidequote != 0 {
return p.parseString()
}
return p.parseSep()
case '\n', ';':
if p.insidequote != 0 {
return p.parseString()
}
return p.parseEol()
case '[':
return p.parseCommand()
case '$':
return p.parseVar()
case '#':
if p.Type == PT_EOL {
p.parseComment()
continue
}
return p.parseString()
default:
return p.parseString()
}
}
return p.token() /* unreached */
}

138
src/picol/picol.go Normal file
View file

@ -0,0 +1,138 @@
package picol
import (
"errors"
"fmt"
"strings"
)
var (
PICOL_RETURN = errors.New("RETURN")
PICOL_BREAK = errors.New("BREAK")
PICOL_CONTINUE = errors.New("CONTINUE")
)
type Var string
type CmdFunc func(i *Interp, argv []string, privdata interface{}) (string, error)
type Cmd struct {
fn CmdFunc
privdata interface{}
}
type CallFrame struct {
vars map[string]Var
parent *CallFrame
}
type Interp struct {
level int
callframe *CallFrame
commands map[string]Cmd
}
func InitInterp() *Interp {
return &Interp{
level: 0,
callframe: &CallFrame{vars: make(map[string]Var)},
commands: make(map[string]Cmd),
}
}
func (i *Interp) Var(name string) (Var, bool) {
for frame := i.callframe; frame != nil; frame = frame.parent {
v, ok := frame.vars[name]
if ok {
return v, ok
}
}
return "", false
}
func (i *Interp) SetVar(name, val string) {
i.callframe.vars[name] = Var(val)
}
func (i *Interp) UnsetVar(name string) {
delete(i.callframe.vars, name)
}
func (i *Interp) Command(name string) *Cmd {
v, ok := i.commands[name]
if !ok {
return nil
}
return &v
}
func (i *Interp) RegisterCommand(name string, fn CmdFunc, privdata interface{}) error {
c := i.Command(name)
if c != nil {
return fmt.Errorf("Command '%s' already defined", name)
}
i.commands[name] = Cmd{fn, privdata}
return nil
}
/* EVAL! */
func (i *Interp) Eval(t string) (string, error) {
p := InitParser(t)
var result string
var err error
argv := []string{}
for {
prevtype := p.Type
// XXX
t = p.GetToken()
if p.Type == PT_EOF {
break
}
switch p.Type {
case PT_VAR:
v, ok := i.Var(t)
if !ok {
return "", fmt.Errorf("No such variable '%s'", t)
}
t = string(v)
case PT_CMD:
result, err = i.Eval(t)
if err != nil {
return result, err
} else {
t = result
}
case PT_ESC:
// XXX: escape handling missing!
case PT_SEP:
prevtype = p.Type
continue
}
// We have a complete command + args. Call it!
if p.Type == PT_EOL {
prevtype = p.Type
if len(argv) != 0 {
c := i.Command(argv[0])
if c == nil {
return "", fmt.Errorf("No such command '%s'", argv[0])
}
result, err = c.fn(i, argv, c.privdata)
if err != nil {
return result, err
}
}
// Prepare for the next command
argv = []string{}
continue
}
// We have a new token, append to the previous or as new arg?
if prevtype == PT_SEP || prevtype == PT_EOL {
argv = append(argv, t)
} else { // Interpolation
argv[len(argv)-1] = strings.Join([]string{argv[len(argv)-1], t}, "")
}
prevtype = p.Type
}
return result, nil
}

85
src/router/router.go Normal file
View file

@ -0,0 +1,85 @@
package router
import (
"fmt"
"log"
"math/rand"
"time"
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
picol "git.dragonheim.net/dragonheim/gagent/src/picol"
zmq "github.com/pebbe/zmq4"
)
func pop(msg []string) (head, tail []string) {
if msg[1] == "" {
head = msg[:2]
tail = msg[2:]
} else {
head = msg[:1]
tail = msg[1:]
}
return
}
// Main is the initiation function for a Router
func Main(config gs.GagentConfig) {
/*
* This is our router task.
*
* It uses the multi-threaded server model to deal requests out to a
* pool of workers and route replies back to clients. One worker can
* handle one request at a time but one client can talk to multiple
* workers at once.
*
* Frontend socket talks to clients over TCP
*/
frontend, _ := zmq.NewSocket(zmq.ROUTER)
defer frontend.Close()
frontend.Bind(fmt.Sprintf("tcp://%s:%d", config.ListenAddr, config.ListenPort))
// Backend socket talks to workers over inproc
backend, _ := zmq.NewSocket(zmq.DEALER)
defer backend.Close()
backend.Bind("inproc://backend")
// Launch pool of worker threads, precise number is not critical
for i := 0; i < 5; i++ {
go agentRouter(i)
}
// Connect backend to frontend via a proxy
err := zmq.Proxy(frontend, backend, nil)
log.Fatalln("Proxy interrupted:", err)
}
// Each worker task works on one request at a time and sends a random number
// of replies back, with random delays between replies:
func agentRouter(workerNum int) {
interp := picol.InitInterp()
interp.RegisterCoreCommands()
worker, _ := zmq.NewSocket(zmq.DEALER)
defer worker.Close()
worker.Connect("inproc://backend")
for {
// The DEALER socket gives us the reply envelope and message
msg, _ := worker.RecvMessage(0)
identity, content := pop(msg)
// Send 0..4 replies back
replies := rand.Intn(5)
for reply := 0; reply < replies; reply++ {
// Sleep for some fraction of a second
time.Sleep(time.Duration(rand.Intn(1000)+1) * time.Millisecond)
fmt.Println(fmt.Sprintf("Worker %d: %s", workerNum, identity))
fmt.Println(fmt.Sprintf("Worker %d: %s", workerNum, content))
worker.SendMessage(identity, content)
}
}
}

75
src/worker/worker.go Normal file
View file

@ -0,0 +1,75 @@
package worker
import (
"fmt"
// "log"
"math/rand"
"time"
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
picol "git.dragonheim.net/dragonheim/gagent/src/picol"
zmq "github.com/pebbe/zmq4"
)
func pop(msg []string) (head, tail []string) {
if msg[1] == "" {
head = msg[:2]
tail = msg[2:]
} else {
head = msg[:1]
tail = msg[1:]
}
return
}
// Main is the initiation function for a Worker
func Main(config gs.GagentConfig) {
// Frontend socket talks to clients over TCP
frontend, _ := zmq.NewSocket(zmq.ROUTER)
defer frontend.Close()
connectString := fmt.Sprintf("tcp://%s", config.Routers[0].RouterAddr)
frontend.Bind(connectString)
// Backend socket talks to workers over inproc
backend, _ := zmq.NewSocket(zmq.DEALER)
defer backend.Close()
backend.Bind("inproc://backend")
// Launch pool of agent handlers
for i := 0; i < 5; i++ {
go agentHandler(i)
}
// Connect backend to frontend via a proxy
// err := zmq.Proxy(frontend, backend, nil)
// log.Fatalln("Proxy interrupted:", err)
}
// Each worker task works on one request at a time and sends a random number
// of replies back, with random delays between replies:
func agentHandler(workerNum int) {
interp := picol.InitInterp()
interp.RegisterCoreCommands()
worker, _ := zmq.NewSocket(zmq.DEALER)
defer worker.Close()
worker.Connect("inproc://backend")
for {
// The DEALER socket gives us the reply envelope and message
msg, _ := worker.RecvMessage(0)
identity, content := pop(msg)
// Send 0..4 replies back
replies := rand.Intn(5)
for reply := 0; reply < replies; reply++ {
// Sleep for some fraction of a second
time.Sleep(time.Duration(rand.Intn(1000)+1) * time.Millisecond)
fmt.Println(fmt.Sprintf("Worker %d: %s", workerNum, identity))
worker.SendMessage(identity, content)
}
}
}