├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── eclus.service ├── eclus.socket ├── eclus.tmpfiles.conf └── src ├── .gitignore └── eclus ├── Makefile ├── cli.go ├── eclus.go ├── enode.go └── esrv.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /eclus 3 | /bin 4 | /pkg 5 | erl_crash.dump 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is the MIT license. 2 | 3 | Copyright (c) 2012-2013 Metachord Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies 13 | or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 17 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH := $(shell pwd) 2 | 3 | all: eclus 4 | 5 | 6 | eclus: 7 | GOPATH=$(GOPATH) go get $@ 8 | GOPATH=$(GOPATH) go build $@ 9 | 10 | clean: 11 | GOPATH=$(GOPATH) go clean 12 | ${RM} -r pkg/ 13 | 14 | .PHONY: eclus 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eclus # 2 | 3 | EPMD replacement in Go 4 | 5 | ## Daemon ## 6 | 7 | To start daemon run: 8 | 9 | ```sh 10 | $ eclus [-port 4369] [-nodes-limit 1000] [-unreg-ttl 10] 11 | ``` 12 | 13 | Flags: 14 | 15 | - `-port`: listening port for daemon, default is 4369 16 | - `-nodes-limit`: capacity size of nodes register, default is 1000 17 | - `-unreg-ttl`: time to live of inactive (down) nodes if register capacity exceed, default is 10 18 | 19 | ## CLI ## 20 | 21 | If `eclus` cannot bind to specified port, it runs in CLI mode. 22 | 23 | To check registered names on epmd, run `eclus` with flag `-names`: 24 | 25 | ```sh 26 | $ eclus -names 27 | asd 50249 7 active 1 Sun Dec 30 03:40:47 2012 28 | gangnam 40937 none down 2 Sun Dec 30 03:44:51 2012 29 | oppa 36677 9 active 2 Sun Dec 30 03:44:48 2012 30 | qwe 60255 none down 1 Sun Dec 30 03:44:25 2012 31 | ``` 32 | 33 | Header is: `| Node name | Port of node | File descriptor of node connection | Node state | Creation counter | Recent state change date |` 34 | 35 | 36 | # Build Status # 37 | 38 | [![GoCI Build Status](http://goci.me/project/image/github.com/metachord/eclus)](http://goci.me/project/github.com/metachord/eclus) 39 | 40 | # Go-node # 41 | 42 | Run eclus with embedded node: 43 | 44 | ```sh 45 | $ eclus -node -node-name 'epmd@localhost' -node-cookie 123asd [-erlang.node.trace] [-erlang.dist.trace] 46 | ``` 47 | 48 | Options `-erlang.node.trace`, `-erlang.dist.trace` will print debug info for correspond subsystems. 49 | 50 | Then run Erlang node with the same cookie: 51 | 52 | ```sh 53 | $ erl -sname asd@localhost -setcookie 123asd 54 | ``` 55 | 56 | ## Ping ## 57 | 58 | Now type `net_adm:ping(epmd@localhost).` in Erlang node: 59 | 60 | ```erlang 61 | (asd@localhost)1> net_adm:ping(epmd@localhost). 62 | pong 63 | ``` 64 | 65 | You see `pong` reply from Go-node! 66 | 67 | ## Implement your own GenServer ## 68 | 69 | See `src/eclus/esrv.go`. It is GenServer behaviour implementation which you can use like original `gen_server` process from Erlang/OTP. 70 | 71 | To run this process first create pointer to structure which implements all methods for this behaviour: 72 | 73 | ```go 74 | eSrv := new(eclusSrv) 75 | ``` 76 | 77 | Then call `Spawn` method on published node: 78 | 79 | ```go 80 | enode.Spawn(eSrv) 81 | ``` 82 | 83 | Now you can interact with this process from Erlang-node using `gen_server:call/2`, gen_server:cast/2` or just send message to it: 84 | 85 | ```erlang 86 | (asd@localhost)3> gen_server:call({eclus, epmd@localhost}, message). 87 | {ok,eclus_reply,message} 88 | ``` 89 | -------------------------------------------------------------------------------- /eclus.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=EPMD Server 3 | After=network.target 4 | Requires=eclus.socket 5 | 6 | [Service] 7 | Type=simple 8 | User=eclus 9 | Group=eclus 10 | WorkingDirectory=/run/eclus 11 | DeviceAllow=/dev/null rw 12 | PrivateTmp=true 13 | NoNewPrivileges=true 14 | Restart=always 15 | LimitFSIZE=0 16 | StandardOutput=journal 17 | StandardError=journal 18 | ExecStart=/usr/bin/eclus 19 | 20 | [Install] 21 | Alias=epmd.service 22 | Also=eclus.socket 23 | -------------------------------------------------------------------------------- /eclus.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=EPMD Server Activation Socket 3 | 4 | [Socket] 5 | ListenStream=4369 6 | 7 | [Install] 8 | WantedBy=sockets.target 9 | -------------------------------------------------------------------------------- /eclus.tmpfiles.conf: -------------------------------------------------------------------------------- 1 | d /run/eclus 0755 eclus eclus 2 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | /github.com 2 | -------------------------------------------------------------------------------- /src/eclus/Makefile: -------------------------------------------------------------------------------- 1 | all clean: 2 | (cd ../../ && ${MAKE} $@) 3 | -------------------------------------------------------------------------------- /src/eclus/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "reflect" 13 | "sort" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | type cliMessageId uint8 19 | 20 | const ( 21 | REQ_NAMES = cliMessageId('N') // 78 22 | ) 23 | 24 | var isCli bool 25 | var isNames bool 26 | 27 | func init() { 28 | flag.BoolVar(&isCli, "cli", false, "CLI enable") 29 | flag.BoolVar(&isNames, "names", false, "(CLI) print nodes info: name | port | fd | state | creation | state change date") 30 | } 31 | 32 | func cliEnabled() bool { 33 | return isCli 34 | } 35 | 36 | func eclusCli() { 37 | epmCli() 38 | } 39 | 40 | func epmCli() { 41 | c, err := net.Dial("tcp", net.JoinHostPort("", listenPort)) 42 | if err != nil { 43 | log.Fatalf("Cannot connect to %s port", listenPort) 44 | } 45 | var req []byte 46 | if isNames { 47 | req = reqNames() 48 | } else { 49 | c.Close() 50 | return 51 | } 52 | c.Write(req) 53 | 54 | buf, err := ioutil.ReadAll(c) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | fmt.Printf(string(buf)) 59 | c.Close() 60 | } 61 | 62 | func reqNames() (req []byte) { 63 | req = make([]byte, 3) 64 | req[2] = byte('N') 65 | binary.BigEndian.PutUint16(req[0:2], 1) 66 | return 67 | } 68 | 69 | func ansNames(nReg map[string]*nodeRec) (reply []byte) { 70 | replyB := new(bytes.Buffer) 71 | mlen := 0 72 | var nodes = make(sort.StringSlice, len(nReg)) 73 | var i int = 0 74 | for nn, _ := range nReg { 75 | nodes[i] = nn 76 | i++ 77 | if len(nn) > mlen { 78 | mlen = len(nn) 79 | } 80 | } 81 | sort.Sort(&nodes) 82 | format := fmt.Sprintf("%%%ds\t%%d\t%%s\t%%s\t%%d\t%%s\n", mlen) 83 | for _, nn := range nodes { 84 | rec := nReg[nn] 85 | sysfd, _ := sysfd(rec.conn) 86 | var sysfdS string 87 | if sysfd < 0 { 88 | sysfdS = "none" 89 | } else { 90 | sysfdS = strconv.Itoa(sysfd) 91 | } 92 | replyB.Write([]byte(fmt.Sprintf(format, rec.Name, rec.Port, sysfdS, actStr(rec.Active), rec.Creation, rec.Time.Format(time.ANSIC)))) 93 | } 94 | reply = replyB.Bytes() 95 | return 96 | } 97 | 98 | func actStr(a bool) (s string) { 99 | if a { 100 | s = "active" 101 | } else { 102 | s = "down" 103 | } 104 | return 105 | } 106 | 107 | func sysfd(c net.Conn) (int, error) { 108 | switch p := c.(type) { 109 | case *net.TCPConn, *net.UDPConn, *net.IPConn: 110 | cv := reflect.ValueOf(p) 111 | switch ce := cv.Elem(); ce.Kind() { 112 | case reflect.Struct: 113 | netfd := ce.FieldByName("conn").FieldByName("fd") 114 | switch fe := netfd.Elem(); fe.Kind() { 115 | case reflect.Struct: 116 | fd := fe.FieldByName("sysfd") 117 | return int(fd.Int()), nil 118 | } 119 | } 120 | return -1, errors.New("invalid conn type") 121 | } 122 | return -1, errors.New("invalid conn type") 123 | } 124 | -------------------------------------------------------------------------------- /src/eclus/eclus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "flag" 7 | "github.com/goerlang/epmd" 8 | "log" 9 | "net" 10 | "os" 11 | "os/signal" 12 | "runtime/pprof" 13 | "strconv" 14 | "time" 15 | "github.com/coreos/go-systemd/activation" 16 | ) 17 | 18 | var noEpmd bool 19 | var listenPort string 20 | var regLimit int 21 | var unregTTL int 22 | var cpuProfile string 23 | 24 | func init() { 25 | flag.StringVar(&listenPort, "port", "4369", "listen port") 26 | flag.BoolVar(&noEpmd, "no-epmd", false, "disable epmd") 27 | flag.IntVar(®Limit, "nodes-limit", 1000, "limit size of registration table to prune unregistered nodes") 28 | flag.IntVar(&unregTTL, "unreg-ttl", 10, "prune unregistered nodes if unregistration older than this value in minutes") 29 | flag.StringVar(&cpuProfile, "profile-cpu", "", "profile CPU to file") 30 | } 31 | 32 | type regAns struct { 33 | reply []byte 34 | isClose bool 35 | } 36 | 37 | type regReq struct { 38 | buf []byte 39 | replyTo chan regAns 40 | conn net.Conn 41 | } 42 | 43 | func main() { 44 | flag.Parse() 45 | if cpuProfile != "" { 46 | f, err := os.Create(cpuProfile) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | pprof.StartCPUProfile(f) 51 | defer pprof.StopCPUProfile() 52 | } 53 | stopCh := make(chan bool) 54 | 55 | c := make(chan os.Signal, 1) 56 | signal.Notify(c, os.Interrupt) 57 | go func() { 58 | for sig := range c { 59 | log.Printf("Signal %#v", sig) 60 | stopCh <- true 61 | } 62 | }() 63 | 64 | if !cliEnabled() { 65 | var err error 66 | var l net.Listener 67 | if !noEpmd { 68 | listen_fds := activation.Files(false) 69 | if listen_fds != nil { 70 | l, err = net.FileListener(listen_fds[0]) 71 | } else { 72 | l, err = net.Listen("tcp", net.JoinHostPort("", listenPort)) 73 | } 74 | if err != nil || noEpmd { 75 | // Cannot bind, eclus instance already running, connect to it 76 | eclusCli() 77 | } else { 78 | epm := make(chan regReq, 10) 79 | go epmReg(epm) 80 | go func() { 81 | for { 82 | conn, err := l.Accept() 83 | log.Printf("Accept new") 84 | if err != nil { 85 | log.Printf(err.Error()) 86 | } else { 87 | go mLoop(conn, epm) 88 | } 89 | } 90 | }() 91 | if nodeEnabled() { 92 | go runNode() 93 | } 94 | <-stopCh 95 | } 96 | } 97 | } else { 98 | eclusCli() 99 | } 100 | } 101 | 102 | type nodeRec struct { 103 | *epmd.NodeInfo 104 | Time time.Time 105 | Active bool 106 | conn net.Conn 107 | } 108 | 109 | func epmReg(in <-chan regReq) { 110 | var nReg = make(map[string]*nodeRec) 111 | for { 112 | select { 113 | case req := <-in: 114 | buf := req.buf 115 | if len(buf) == 0 { 116 | rs := len(nReg) 117 | log.Printf("REG %d records", rs) 118 | now := time.Now() 119 | 120 | for node, rec := range nReg { 121 | if rec.conn == req.conn { 122 | log.Printf("Connection for %s dropped", node) 123 | nReg[node].Active = false 124 | nReg[node].Time = now 125 | nReg[node].conn = nil 126 | } else if rs > regLimit && !rec.Active && now.Sub(rec.Time).Minutes() > float64(unregTTL) { 127 | log.Printf("REG prune %s:%+v", node, rec) 128 | delete(nReg, node) 129 | } 130 | } 131 | continue 132 | } 133 | replyTo := req.replyTo 134 | log.Printf("IN: %v", buf) 135 | switch epmd.MessageId(buf[0]) { 136 | case epmd.ALIVE2_REQ: 137 | nConn := req.conn 138 | nInfo := epmd.Read_ALIVE2_REQ(buf) 139 | log.Printf("NodeInfo: %+v", nInfo) 140 | var reply []byte 141 | 142 | if rec, ok := nReg[nInfo.Name]; ok { 143 | log.Printf("Node %s found", nInfo.Name) 144 | if rec.Active { 145 | log.Printf("Node %s is running", nInfo.Name) 146 | reply = epmd.Compose_ALIVE2_RESP(false) 147 | } else { 148 | log.Printf("Node %s is not running", nInfo.Name) 149 | rec.conn = nConn 150 | 151 | nInfo.Creation = (rec.Creation % 3) + 1 152 | rec.NodeInfo = nInfo 153 | rec.Active = true 154 | reply = epmd.Compose_ALIVE2_RESP(true, rec.NodeInfo) 155 | } 156 | } else { 157 | log.Printf("New node %s", nInfo.Name) 158 | nInfo.Creation = 1 159 | rec := &nodeRec{ 160 | NodeInfo: nInfo, 161 | conn: nConn, 162 | Time: time.Now(), 163 | Active: true, 164 | } 165 | nReg[nInfo.Name] = rec 166 | reply = epmd.Compose_ALIVE2_RESP(true, rec.NodeInfo) 167 | } 168 | replyTo <- regAns{reply: reply, isClose: false} 169 | case epmd.PORT_PLEASE2_REQ: 170 | nName := epmd.Read_PORT_PLEASE2_REQ(buf) 171 | var reply []byte 172 | if rec, ok := nReg[nName]; ok && rec.Active { 173 | reply = epmd.Compose_PORT2_RESP(rec.NodeInfo) 174 | } else { 175 | reply = epmd.Compose_PORT2_RESP(nil) 176 | } 177 | replyTo <- regAns{reply: reply, isClose: true} 178 | case epmd.STOP_REQ: 179 | nName := epmd.Read_STOP_REQ(buf) 180 | var reply []byte 181 | if rec, ok := nReg[nName]; ok && rec.Active { 182 | // TODO: stop node 183 | reply = epmd.Compose_STOP_RESP(true) 184 | } else { 185 | reply = epmd.Compose_STOP_RESP(false) 186 | } 187 | replyTo <- regAns{reply: reply, isClose: true} 188 | case epmd.NAMES_REQ, epmd.DUMP_REQ: 189 | lp, err := strconv.Atoi(listenPort) 190 | if err != nil { 191 | log.Printf("Cannot convert %s to integer", listenPort) 192 | replyTo <- regAns{reply: nil, isClose: true} 193 | } else { 194 | replyB := new(bytes.Buffer) 195 | epmd.Compose_START_NAMES_RESP(replyB, lp) 196 | for _, rec := range nReg { 197 | if epmd.MessageId(buf[0]) == epmd.NAMES_REQ { 198 | if rec.Active { 199 | epmd.Append_NAMES_RESP(replyB, rec.NodeInfo) 200 | } else { 201 | if rec.Active { 202 | epmd.Append_DUMP_RESP_ACTIVE(replyB, rec.NodeInfo) 203 | } else { 204 | epmd.Append_DUMP_RESP_UNUSED(replyB, rec.NodeInfo) 205 | } 206 | } 207 | } 208 | } 209 | replyTo <- regAns{reply: replyB.Bytes(), isClose: true} 210 | } 211 | case epmd.KILL_REQ: 212 | reply := epmd.Compose_KILL_RESP() 213 | replyTo <- regAns{reply: reply, isClose: true} 214 | default: 215 | switch cliMessageId(buf[0]) { 216 | case REQ_NAMES: 217 | reply := ansNames(nReg) 218 | replyTo <- regAns{reply: reply, isClose: true} 219 | default: 220 | replyTo <- regAns{reply: nil, isClose: true} 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | func mLoop(c net.Conn, epm chan regReq) { 228 | buf := make([]byte, 1024) 229 | for { 230 | n, err := c.Read(buf) 231 | if err != nil { 232 | c.Close() 233 | log.Printf("Stop loop: %v", err) 234 | epm <- regReq{buf: []byte{}, conn: c} 235 | return 236 | } 237 | length := binary.BigEndian.Uint16(buf[0:2]) 238 | if length != uint16(n-2) { 239 | log.Printf("Incomplete packet from erlang node to epmd: %d from %d", n, length) 240 | break 241 | } 242 | log.Printf("Read %d, %d: %v", n, length, buf[2:n]) 243 | if isClose := handleMsg(c, buf[2:n], epm); isClose { 244 | break 245 | } 246 | } 247 | c.Close() 248 | } 249 | 250 | func handleMsg(c net.Conn, buf []byte, epm chan regReq) bool { 251 | myChan := make(chan regAns) 252 | epm <- regReq{buf: buf, replyTo: myChan, conn: c} 253 | select { 254 | case ans := <-myChan: 255 | log.Printf("Got reply: %+v", ans) 256 | if ans.reply != nil { 257 | c.Write(ans.reply) 258 | } 259 | return ans.isClose 260 | } 261 | return true 262 | } 263 | -------------------------------------------------------------------------------- /src/eclus/enode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/goerlang/etf" 6 | "github.com/goerlang/node" 7 | "log" 8 | ) 9 | 10 | var enableNode bool 11 | var nodeName string 12 | var nodeCookie string 13 | var nodePort int 14 | 15 | func init() { 16 | flag.BoolVar(&enableNode, "node", false, "start erlang node") 17 | flag.StringVar(&nodeName, "node-name", "", "name of erlang node") 18 | flag.StringVar(&nodeCookie, "node-cookie", "", "cookie of erlang node") 19 | flag.IntVar(&nodePort, "node-port", 5858, "port of erlang node") 20 | } 21 | 22 | func nodeEnabled() bool { 23 | return enableNode 24 | } 25 | 26 | func runNode() (enode *node.Node) { 27 | enode = node.NewNode(nodeName, nodeCookie) 28 | err := enode.Publish(nodePort) 29 | if err != nil { 30 | log.Printf("Cannot publish: %s", err) 31 | enode = nil 32 | } 33 | eSrv := new(eclusSrv) 34 | enode.Spawn(eSrv) 35 | 36 | eClos := func(terms etf.List) (r etf.Term) { 37 | r = etf.Term(etf.Tuple{etf.Atom("enode"), len(terms)}) 38 | return 39 | } 40 | 41 | err = enode.RpcProvide("enode", "lambda", eClos) 42 | if err != nil { 43 | log.Printf("Cannot provide function to RPC: %s", err) 44 | } 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /src/eclus/esrv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/goerlang/etf" 5 | "github.com/goerlang/node" 6 | "log" 7 | ) 8 | 9 | type eclusSrv struct { 10 | node.GenServerImpl 11 | } 12 | 13 | func (es *eclusSrv) Init(args ...interface{}) { 14 | log.Printf("ECLUS_SRV: Init: %#v", args) 15 | es.Node.Register(etf.Atom("eclus"), es.Self) 16 | } 17 | 18 | func (es *eclusSrv) HandleCast(message *etf.Term) { 19 | log.Printf("ECLUS_SRV: HandleCast: %#v", *message) 20 | } 21 | 22 | func (es *eclusSrv) HandleCall(message *etf.Term, from *etf.Tuple) (reply *etf.Term) { 23 | log.Printf("ECLUS_SRV: HandleCall: %#v, From: %#v", *message, *from) 24 | replyTerm := etf.Term(etf.Tuple{etf.Atom("ok"), etf.Atom("eclus_reply"), *message}) 25 | reply = &replyTerm 26 | return 27 | } 28 | 29 | func (es *eclusSrv) HandleInfo(message *etf.Term) { 30 | log.Printf("ECLUS_SRV: HandleInfo: %#v", *message) 31 | } 32 | 33 | func (es *eclusSrv) Terminate(reason interface{}) { 34 | log.Printf("ECLUS_SRV: Terminate: %#v", reason.(int)) 35 | } 36 | --------------------------------------------------------------------------------