Length prefixed tmsp messages; counter app in javascriptpull/1780/head
@ -0,0 +1,270 @@ | |||
package main | |||
import ( | |||
"bufio" | |||
"encoding/hex" | |||
"fmt" | |||
"io" | |||
"net" | |||
"os" | |||
"strings" | |||
. "github.com/tendermint/go-common" | |||
"github.com/tendermint/go-wire" | |||
"github.com/tendermint/tmsp/types" | |||
"github.com/codegangsta/cli" | |||
) | |||
// connection is a global variable so it can be reused by the console | |||
var conn net.Conn | |||
func main() { | |||
app := cli.NewApp() | |||
app.Name = "cli" | |||
app.Usage = "cli [command] [args...]" | |||
app.Flags = []cli.Flag{ | |||
cli.StringFlag{ | |||
Name: "address", | |||
Value: "tcp://127.0.0.1:46658", | |||
Usage: "address of application socket", | |||
}, | |||
} | |||
app.Commands = []cli.Command{ | |||
{ | |||
Name: "batch", | |||
Usage: "Run a batch of tmsp commands against an application", | |||
Action: func(c *cli.Context) { | |||
cmdBatch(app, c) | |||
}, | |||
}, | |||
{ | |||
Name: "console", | |||
Usage: "Start an interactive tmsp console for multiple commands", | |||
Action: func(c *cli.Context) { | |||
cmdConsole(app, c) | |||
}, | |||
}, | |||
{ | |||
Name: "echo", | |||
Usage: "Have the application echo a message", | |||
Action: func(c *cli.Context) { | |||
cmdEcho(c) | |||
}, | |||
}, | |||
{ | |||
Name: "info", | |||
Usage: "Get some info about the application", | |||
Action: func(c *cli.Context) { | |||
cmdInfo(c) | |||
}, | |||
}, | |||
{ | |||
Name: "set_option", | |||
Usage: "Set an option on the application", | |||
Action: func(c *cli.Context) { | |||
cmdSetOption(c) | |||
}, | |||
}, | |||
{ | |||
Name: "append_tx", | |||
Usage: "Append a new tx to application", | |||
Action: func(c *cli.Context) { | |||
cmdAppendTx(c) | |||
}, | |||
}, | |||
{ | |||
Name: "get_hash", | |||
Usage: "Get application Merkle root hash", | |||
Action: func(c *cli.Context) { | |||
cmdGetHash(c) | |||
}, | |||
}, | |||
{ | |||
Name: "commit", | |||
Usage: "Commit the application state", | |||
Action: func(c *cli.Context) { | |||
cmdCommit(c) | |||
}, | |||
}, | |||
{ | |||
Name: "rollback", | |||
Usage: "Roll back the application state to the latest commit", | |||
Action: func(c *cli.Context) { | |||
cmdRollback(c) | |||
}, | |||
}, | |||
} | |||
app.Before = before | |||
app.Run(os.Args) | |||
} | |||
func before(c *cli.Context) error { | |||
if conn == nil { | |||
var err error | |||
conn, err = Connect(c.GlobalString("address")) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
} | |||
return nil | |||
} | |||
//-------------------------------------------------------------------------------- | |||
func cmdBatch(app *cli.App, c *cli.Context) { | |||
bufReader := bufio.NewReader(os.Stdin) | |||
for { | |||
line, more, err := bufReader.ReadLine() | |||
if more { | |||
Exit("input line is too long") | |||
} else if err == io.EOF { | |||
break | |||
} else if len(line) == 0 { | |||
continue | |||
} else if err != nil { | |||
Exit(err.Error()) | |||
} | |||
args := []string{"tmsp"} | |||
args = append(args, strings.Split(string(line), " ")...) | |||
app.Run(args) | |||
} | |||
} | |||
func cmdConsole(app *cli.App, c *cli.Context) { | |||
for { | |||
fmt.Printf("> ") | |||
bufReader := bufio.NewReader(os.Stdin) | |||
line, more, err := bufReader.ReadLine() | |||
if more { | |||
Exit("input is too long") | |||
} else if err != nil { | |||
Exit(err.Error()) | |||
} | |||
args := []string{"tmsp"} | |||
args = append(args, strings.Split(string(line), " ")...) | |||
app.Run(args) | |||
} | |||
} | |||
// Have the application echo a message | |||
func cmdEcho(c *cli.Context) { | |||
args := c.Args() | |||
if len(args) != 1 { | |||
Exit("echo takes 1 argument") | |||
} | |||
res, err := makeRequest(conn, types.RequestEcho{args[0]}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Println(res) | |||
} | |||
// Get some info from the application | |||
func cmdInfo(c *cli.Context) { | |||
res, err := makeRequest(conn, types.RequestInfo{}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Println(res) | |||
} | |||
// Set an option on the application | |||
func cmdSetOption(c *cli.Context) { | |||
args := c.Args() | |||
if len(args) != 2 { | |||
Exit("set_option takes 2 arguments (key, value)") | |||
} | |||
_, err := makeRequest(conn, types.RequestSetOption{args[0], args[1]}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Printf("%s=%s\n", args[0], args[1]) | |||
} | |||
// Append a new tx to application | |||
func cmdAppendTx(c *cli.Context) { | |||
args := c.Args() | |||
if len(args) != 1 { | |||
Exit("append_tx takes 1 argument") | |||
} | |||
txString := args[0] | |||
tx := []byte(txString) | |||
if len(txString) > 2 && strings.HasPrefix(txString, "0x") { | |||
var err error | |||
tx, err = hex.DecodeString(txString[2:]) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
} | |||
res, err := makeRequest(conn, types.RequestAppendTx{tx}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Println("Response:", res) | |||
} | |||
// Get application Merkle root hash | |||
func cmdGetHash(c *cli.Context) { | |||
res, err := makeRequest(conn, types.RequestGetHash{}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Printf("%X\n", res.(types.ResponseGetHash).Hash) | |||
} | |||
// Commit the application state | |||
func cmdCommit(c *cli.Context) { | |||
_, err := makeRequest(conn, types.RequestCommit{}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Println("Committed.") | |||
} | |||
// Roll back the application state to the latest commit | |||
func cmdRollback(c *cli.Context) { | |||
_, err := makeRequest(conn, types.RequestRollback{}) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
fmt.Println("Rolled back.") | |||
} | |||
//-------------------------------------------------------------------------------- | |||
func makeRequest(conn net.Conn, req types.Request) (types.Response, error) { | |||
var n int | |||
var err error | |||
// Write desired request | |||
wire.WriteBinaryLengthPrefixed(req, conn, &n, &err) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Write flush request | |||
wire.WriteBinaryLengthPrefixed(types.RequestFlush{}, conn, &n, &err) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Read desired response | |||
var res types.Response | |||
wire.ReadBinaryPtrLengthPrefixed(&res, conn, 0, &n, &err) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Read flush response | |||
var resFlush types.ResponseFlush | |||
wire.ReadBinaryPtrLengthPrefixed(&resFlush, conn, 0, &n, &err) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return res, nil | |||
} |
@ -0,0 +1,87 @@ | |||
server = require("./server") | |||
wire = require("./wire") | |||
util = require("util") | |||
function CounterApp(){ | |||
this.hashCount = 0; | |||
this.txCount = 0; | |||
this.commitCount = 0; | |||
}; | |||
CounterApp.prototype.open = function(){ | |||
return new CounterAppContext(this); | |||
} | |||
function CounterAppContext(app) { | |||
this.hashCount = app.hashCount; | |||
this.txCount = app.txCount; | |||
this.commitCount = app.commitCount; | |||
this.serial = false; | |||
} | |||
CounterAppContext.prototype.echo = function(msg){ | |||
return {"response": msg, "ret_code":0} | |||
} | |||
CounterAppContext.prototype.info = function(){ | |||
return {"response": [util.format("hash, tx, commit counts: %d, %d, %d", this.hashCount, this.txCount, this.commitCount)]} | |||
} | |||
CounterAppContext.prototype.set_option = function(key, value){ | |||
if (key == "serial" && value == "on"){ | |||
this.serial = true; | |||
} | |||
return {"ret_code":0} | |||
} | |||
CounterAppContext.prototype.append_tx = function(txBytes){ | |||
if (this.serial) { | |||
txByteArray = new Buffer(txBytes) | |||
if (txBytes.length >= 2 && txBytes.slice(0, 2) == "0x") { | |||
txByteArray = wire.hex2bytes(txBytes.slice(2)); | |||
} | |||
r = new msg.buffer(txByteArray) | |||
txValue = wire.decode_big_endian(r, txBytes.length) | |||
if (txValue != this.txCount){ | |||
return {"ret_code":1} | |||
} | |||
} | |||
this.txCount += 1; | |||
return {"ret_code":0} // TODO: return events | |||
} | |||
CounterAppContext.prototype.get_hash = function(){ | |||
this.hashCount += 1; | |||
if (this.txCount == 0){ | |||
return {"response": "", "ret_code":0} | |||
} | |||
h = wire.encode_big_endian(this.txCount, 8); | |||
h = wire.reverse(h); // TODO | |||
return {"response": h.toString(), "ret_code":0} | |||
} | |||
CounterAppContext.prototype.commit = function(){ | |||
this.commitCount += 1; | |||
return {"ret_code":0} | |||
} | |||
CounterAppContext.prototype.rollback = function(){ | |||
return {"ret_code":0} | |||
} | |||
CounterAppContext.prototype.add_listener = function(){ | |||
return {"ret_code":0} | |||
} | |||
CounterAppContext.prototype.rm_listener = function(){ | |||
return {"ret_code":0} | |||
} | |||
CounterAppContext.prototype.event = function(){ | |||
} | |||
console.log("Counter app in Javascript") | |||
var app = new CounterApp(); | |||
var appServer = new server.AppServer(app); | |||
appServer.server.listen(46658) |
@ -0,0 +1,64 @@ | |||
wire = require("./wire") | |||
module.exports = { | |||
types : { | |||
0x01 : "echo", | |||
0x02 : "flush", | |||
0x03 : "info", | |||
0x04 : "set_option", | |||
0x21 : "append_tx", | |||
0x22 : "get_hash", | |||
0x23 : "commit", | |||
0x24 : "rollback", | |||
0x25 : "add_listener", | |||
0x26 : "rm_listener", | |||
}, | |||
decoder : RequestDecoder, | |||
buffer: BytesBuffer | |||
} | |||
function RequestDecoder(buf){ | |||
this.buf= buf | |||
} | |||
var decode_string = wire.decode_string | |||
// return nothing, one thing, or a list of things | |||
RequestDecoder.prototype.echo = function(){ return decode_string(this.buf) }; | |||
RequestDecoder.prototype.flush = function(){}; | |||
RequestDecoder.prototype.info = function(){}; | |||
RequestDecoder.prototype.set_option = function(){ return [decode_string(this.buf), decode_string(this.buf)] }; | |||
RequestDecoder.prototype.append_tx = function(){ return decode_string(this.buf)}; | |||
RequestDecoder.prototype.get_hash = function(){ }; | |||
RequestDecoder.prototype.commit = function(){ }; | |||
RequestDecoder.prototype.rollback = function(){ }; | |||
RequestDecoder.prototype.add_listener = function(){ }; // TODO | |||
RequestDecoder.prototype.rm_listener = function(){ }; // TODO | |||
// buffered reader with read(n) method | |||
function BytesBuffer(buf){ | |||
this.buf = buf | |||
} | |||
BytesBuffer.prototype.read = function(n){ | |||
b = this.buf.slice(0, n) | |||
this.buf = this.buf.slice(n) | |||
return b | |||
}; | |||
BytesBuffer.prototype.write = function(buf){ | |||
this.buf = Buffer.concat([this.buf, buf]); | |||
}; | |||
BytesBuffer.prototype.size = function(){ | |||
return this.buf.length | |||
} | |||
BytesBuffer.prototype.peek = function(){ | |||
return this.buf[0] | |||
} | |||
@ -0,0 +1,128 @@ | |||
// Load the TCP Library | |||
net = require('net'); | |||
msg = require('./msgs'); | |||
wire = require("./wire") | |||
// Takes an application and handles tmsp connection | |||
// which invoke methods on the app | |||
function AppServer(app){ | |||
// set the app for the socket handler | |||
this.app = app; | |||
// create a server by providing callback for | |||
// accepting new connection and callbacks for | |||
// connection events ('data', 'end', etc.) | |||
this.createServer() | |||
} | |||
module.exports = { AppServer: AppServer }; | |||
AppServer.prototype.createServer = function(){ | |||
app = this.app | |||
conns = {} // map sockets to their state | |||
// define the socket handler | |||
this.server = net.createServer(function(socket){ | |||
socket.name = socket.remoteAddress + ":" + socket.remotePort | |||
console.log("new connection from", socket.name) | |||
appCtx = app.open() | |||
var conn = { | |||
recBuf: new msg.buffer(new Buffer(0)), | |||
resBuf: new msg.buffer(new Buffer(0)), | |||
msgLength: 0, | |||
inProgress: false | |||
} | |||
conns[socket] = conn | |||
// Handle tmsp requests. | |||
socket.on('data', function (data) { | |||
if (data.length == 0){ | |||
// TODO err | |||
console.log("empty data!") | |||
return | |||
} | |||
conn = conns[socket] | |||
// we received data. append it | |||
conn.recBuf.write(data) | |||
while ( conn.recBuf.size() > 0 ){ | |||
if (conn.msgLength == 0){ | |||
ll = conn.recBuf.peek(); | |||
if (conn.recBuf.size() < 1 + ll){ | |||
// don't have enough bytes to read length yet | |||
return | |||
} | |||
conn.msgLength = wire.decode_varint(conn.recBuf) | |||
} | |||
if (conn.recBuf.size() < conn.msgLength) { | |||
// don't have enough to decode the message | |||
return | |||
} | |||
// now we can decode | |||
typeByte = conn.recBuf.read(1); | |||
resTypeByte = typeByte[0] + 0x10 | |||
reqType = msg.types[typeByte[0]]; | |||
if (reqType == "flush"){ | |||
// msgs are length prefixed | |||
conn.resBuf.write(wire.encode(1)); | |||
conn.resBuf.write(new Buffer([resTypeByte])) | |||
n = socket.write(conn.resBuf.buf); | |||
conn.msgLength = 0; | |||
conn.resBuf = new msg.buffer(new Buffer(0)); | |||
return | |||
} | |||
// decode args | |||
decoder = new msg.decoder(conn.recBuf); | |||
args = decoder[reqType](); | |||
// done decoding | |||
conn.msgLength = 0 | |||
var res = function(){ | |||
if (args == null){ | |||
return appCtx[reqType](); | |||
} else if (Array.isArray(args)){ | |||
return appCtx[reqType].apply(appCtx, args); | |||
} else { | |||
return appCtx[reqType](args) | |||
} | |||
}() | |||
var retCode = res["ret_code"] | |||
var res = res["response"] | |||
if (retCode != null && retCode != 0){ | |||
console.log("non-zero ret code", retCode) | |||
} | |||
if (reqType == "echo" || reqType == "info"){ | |||
enc = Buffer.concat([new Buffer([resTypeByte]), wire.encode(res)]); | |||
// length prefixed | |||
conn.resBuf.write(wire.encode(enc.length)); | |||
conn.resBuf.write(enc); | |||
} else { | |||
enc = Buffer.concat([new Buffer([resTypeByte]), wire.encode(retCode), wire.encode(res)]); | |||
conn.resBuf.write(wire.encode(enc.length)); | |||
conn.resBuf.write(enc); | |||
} | |||
} | |||
}); | |||
socket.on('end', function () { | |||
console.log("connection ended") | |||
}); | |||
}) | |||
} | |||
@ -0,0 +1,113 @@ | |||
math = require("math") | |||
module.exports = { | |||
decode_string: decode_string, | |||
decode_varint: decode_varint, | |||
decode_big_endian: decode_big_endian, | |||
encode_big_endian: encode_big_endian, | |||
encode: encode, | |||
reverse: reverse, | |||
} | |||
function reverse(buf){ | |||
for (var i = 0; i < buf.length/2; i++){ | |||
a = buf[i]; | |||
b = buf[buf.length-1 - i]; | |||
buf[i] = b; | |||
buf[buf.length-1 - i] = a; | |||
} | |||
return buf | |||
} | |||
function uvarint_size(i){ | |||
if (i == 0){ | |||
return 0 | |||
} | |||
for(var j = 1; j < 9; j++) { | |||
if ( i < 1<<j*8 ) { | |||
return j | |||
} | |||
} | |||
return 8 | |||
} | |||
function encode_big_endian(i, size){ | |||
if (size == 0){ | |||
return new Buffer(0); | |||
} | |||
b = encode_big_endian(math.floor(i/256), size-1); | |||
return Buffer.concat([b, new Buffer([i%256])]); | |||
} | |||
function decode_big_endian(reader, size){ | |||
if (size == 0){ return 0 } | |||
firstByte = reader.read(1)[0]; | |||
return firstByte*(math.pow(256, size-1)) + decode_big_endian(reader, size-1) | |||
} | |||
function encode_string(s){ | |||
size = encode_varint(s.length); | |||
return Buffer.concat([size, new Buffer(s)]) | |||
} | |||
function decode_string(reader){ | |||
length = decode_varint(reader); | |||
return reader.read(length).toString() | |||
} | |||
function encode_varint(i){ | |||
var negate = false; | |||
if (i < 0){ | |||
negate = true; | |||
i = -i; | |||
} | |||
size = uvarint_size(i); | |||
if (size == 0){ | |||
return new Buffer([0]) | |||
} | |||
big_end = encode_big_endian(i, size); | |||
if (negate){ size += 0xF0 } | |||
var buf = new Buffer([1]); | |||
return Buffer.concat([buf, big_end]) | |||
} | |||
function decode_varint(reader){ | |||
size = reader.read(1)[0]; | |||
if (size == 0 ){ | |||
return 0 | |||
} | |||
var negate = false; | |||
if (size > 0xF0){ negate = true } | |||
if (negate) { size = size - 0xF0 } | |||
i = decode_big_endian(reader, size); | |||
if (negate) { i = i * -1} | |||
return i | |||
} | |||
function encode_list(l){ | |||
var l2 = l.map(encode); | |||
var buf = new Buffer(encode_varint(l2.length)); | |||
return Buffer.concat([buf, Buffer.concat(l2)]); | |||
} | |||
function encode(b){ | |||
if (b == null){ | |||
return Buffer(0) | |||
} else if (typeof b == "number"){ | |||
return encode_varint(b) | |||
} else if (typeof b == "string"){ | |||
return encode_string(b) | |||
} else if (Array.isArray(b)){ | |||
return encode_list(b) | |||
} else{ | |||
console.log("UNSUPPORTED TYPE!", typeof b, b) | |||
} | |||
} | |||