em-rserve 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in em-rserve.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,26 @@
1
+
2
+ EM::Rserve is an attempt to bring RServe over EventMachine.
3
+ Consider it a somewhat stable version.
4
+
5
+ EM::Rserve is pure-ruby and should work wherever EventMachine is supported.
6
+
7
+ So far it can:
8
+ - connect to a server
9
+ - detach and reattach a session
10
+ - parse most low-level messages
11
+ - parse most R' S-expressions to a ruby tree
12
+ - translate several common R' S-expressions to "base" ruby objects such as arrays, strings, hashes ...
13
+ - translate several Ruby objects (arrays of integers, strings etc.) to R' S-expression
14
+ - evaluate strings of R code
15
+ - handle pools of connections with ruby fibers
16
+
17
+ Limitations:
18
+ - large QAP1 messages (>8MB) are not supported, this limitation means that you cannot transfer large object directly over RServe protocol
19
+ - no support for the connections with a password. somehow, password without encryption defeats the purpose. hence, if you need a password, you should setup an SSH proxy instead
20
+
21
+ Links:
22
+ - http://www.r-project.org/
23
+ - http://www.rforge.net/Rserve/
24
+ - http://rubyeventmachine.com/
25
+
26
+ Contributions welcome.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/TODO ADDED
@@ -0,0 +1,14 @@
1
+ * missing types
2
+ * special handling in ruby2r translator
3
+ - Factor
4
+ - Table
5
+ * RESP_ERR as a flag and not an OR-ed value
6
+ -> will need to modify Header#error? and Header#error
7
+ * support for long (>8M) messages
8
+ * login command
9
+ support crypt and store salt from connection setup
10
+ * better attach method
11
+ * write doc
12
+ * more spec once the RServe spec is better understood/have enough examples
13
+ * subclass Request for each commmand instead of current quick'n dirty mechanism
14
+ * better test requests stacking/unstacking
data/em-rserve.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "em-rserve/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "em-rserve"
7
+ s.version = EM::Rserve::VERSION
8
+ s.authors = ["crapooze"]
9
+ s.email = ["crapooze@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{An EventMachine client for RServe}
12
+ s.description = %q{Do evented stats with EventMachine and RServe}
13
+
14
+ s.rubyforge_project = "em-rserve"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ end
@@ -0,0 +1,84 @@
1
+
2
+ require "em-rserve/protocol/connector"
3
+ require "em-rserve/protocol/parser"
4
+ require "em-rserve/protocol/request"
5
+ require "em-rserve/qap1/constants"
6
+ require "em-rserve/qap1/header"
7
+ require "em-rserve/qap1/message"
8
+
9
+ module EM::Rserve
10
+ class Connection < EM::Connection
11
+ include Protocol::Connector
12
+ include QAP1
13
+
14
+ def shutdown!(&blk)
15
+ header = Header.new(Constants::CMD_shutdown,0,0,0)
16
+ send_data header.to_bin
17
+
18
+ request(&blk)
19
+ end
20
+
21
+ def r_eval(string, void=false,&blk)
22
+ data = Message.encode_string(string)
23
+ if void
24
+ header = Header.new(0x0002, data.length, 0, 0)
25
+ else
26
+ header = Header.new(0x0003, data.length, 0, 0)
27
+ end
28
+ send_data header.to_bin
29
+ send_data data
30
+
31
+ request(&blk)
32
+ end
33
+
34
+ def login(user,pwd, crypted=true, &blk)
35
+ raise NotImplementedError, "will come later"
36
+ #XXX need to read the salt during connection setup
37
+ cifer = crypted ? pwd : crypt(pwd, salt)
38
+ data = Message.encode_string([user, cifer].join("\n"))
39
+ header = Header.new(Constants::CMD_login, data.length, 0, 0)
40
+ send_data header.to_bin
41
+ send_data data
42
+
43
+ request(&blk)
44
+ end
45
+
46
+ def detach(&blk)
47
+ header = Header.new(Constants::CMD_detachSession, 0, 0, 0)
48
+ send_data header.to_bin #port, key of 20 bytes
49
+
50
+ request(&blk)
51
+ end
52
+
53
+ def attach(key, &blk)
54
+ #XXX it seems that there is no need to send a Header + Message, and
55
+ #raw_writing because the server does a read of 32 bytes on newly accepted
56
+ #connections
57
+ raise ArgumentError, "wrong key length, Rserve wants 32bytes" unless key.size == 32
58
+ send_data key
59
+
60
+ request(&blk)
61
+ end
62
+
63
+ def assign(symbol, sexp_node, parse_symbol_name=true, &blk)
64
+ data = Message.new([symbol.to_s, sexp_node]).to_bin
65
+ data << "\xFF" * data.length % 4
66
+ header = if parse_symbol_name
67
+ Header.new(Constants::CMD_setSEXP, data.length, 0, 0)
68
+ else
69
+ Header.new(Constants::CMD_assignSEXP, data.length, 0, 0)
70
+ end
71
+ send_data header.to_bin
72
+ send_data data
73
+
74
+ request(&blk)
75
+ end
76
+
77
+ # MISSING:
78
+ # - open/close/delete/read/write files
79
+ # - set encoding
80
+ # - set buffer size
81
+ # - control commands
82
+ # - serial commands
83
+ end
84
+ end
@@ -0,0 +1,66 @@
1
+
2
+ require "fiber"
3
+ require "em-rserve/connection"
4
+
5
+ module EM::Rserve
6
+ class FiberedConnection < EM::Rserve::Connection
7
+ attr_accessor :fiber
8
+
9
+ def self.start(fiber=Fiber.current, *args)
10
+ conn = super(*args)
11
+ conn.fiber = fiber
12
+ Fiber.yield
13
+ end
14
+
15
+ def ready
16
+ super
17
+ fiber.resume self if fiber
18
+ end
19
+
20
+ def call(script, *args)
21
+ r_eval(script.to_s, *args) do |req|
22
+ req.errback do |err|
23
+ fiber.resume nil
24
+ end
25
+
26
+ req.callback do |msg|
27
+ unless msg
28
+ fiber.resume nil
29
+ next
30
+ end
31
+ root = msg.parameters.first
32
+ if root
33
+ fiber.resume EM::Rserve::R::RtoRuby::Translator.r_to_ruby(root)
34
+ else
35
+ fiber.resume nil
36
+ end
37
+ end
38
+ end
39
+
40
+ Fiber.yield
41
+ end
42
+
43
+ def set(sym, val)
44
+ root = EM::Rserve::R::RubytoR::Translator.ruby_to_r val
45
+
46
+ assign(sym, root) do |req|
47
+ req.errback do |err|
48
+ fiber.resume nil
49
+ end
50
+ req.callback do |msg|
51
+ fiber.resume val
52
+ end
53
+ end
54
+
55
+ Fiber.yield
56
+ end
57
+
58
+ # *WARNING* this method is unsafe always check your input parameter
59
+ def get(sym)
60
+ call(sym)
61
+ end
62
+
63
+ alias :[]= :set
64
+ alias :[] :get
65
+ end
66
+ end
@@ -0,0 +1,76 @@
1
+
2
+ require "em-rserve/fibered_connection"
3
+
4
+ module EM::Rserve
5
+ class Pooler
6
+ class << self
7
+ def r(klass=FiberedConnection)
8
+ Fiber.new do
9
+ begin
10
+ conn = klass.start
11
+ yield conn
12
+ ensure
13
+ conn.close_connection
14
+ end
15
+ end.resume
16
+ end
17
+ end
18
+
19
+ attr_reader :connections, :size, :connection_class
20
+ def initialize(size=10, klass=FiberedConnection)
21
+ @connections = []
22
+ @size = size
23
+ @connection_class = klass
24
+ end
25
+
26
+ def empty?
27
+ connections.empty?
28
+ end
29
+
30
+ def full?
31
+ connections.size >= size
32
+ end
33
+
34
+ def connection
35
+ #XXX duplicated code from Pooler.r to avoid proc-ing the blk
36
+ Fiber.new do
37
+ yield connection_class.start
38
+ end.resume
39
+ end
40
+
41
+ def r
42
+ conn = connections.shift
43
+ if conn
44
+ Fiber.new do
45
+ begin
46
+ conn.fiber = Fiber.current
47
+ yield conn
48
+ ensure
49
+ conn.close_connection
50
+ end
51
+ fill 1 unless full?
52
+ end.resume
53
+ else
54
+ fill size
55
+ connection do |conn|
56
+ begin
57
+ yield conn
58
+ ensure
59
+ conn.close_connection
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def preconnect!
66
+ connection do |conn|
67
+ conn.fiber = nil
68
+ connections << conn
69
+ end
70
+ end
71
+
72
+ def fill(n=size)
73
+ n.times{preconnect!}
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,89 @@
1
+
2
+ require "em-rserve/protocol/parser"
3
+ require "em-rserve/protocol/request"
4
+
5
+ module EM::Rserve
6
+ module Protocol
7
+ module Connector
8
+ def self.included(obj)
9
+ obj.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def start(server='127.0.0.1', port=6311)
14
+ EM.connect(server, port, self)
15
+ end
16
+ end
17
+
18
+ extend ClassMethods
19
+
20
+ attr_accessor :request_queue
21
+
22
+ def post_init
23
+ super
24
+ #type of parser carries the state, no need to carry it internally and do
25
+ #zillions of state check
26
+ @parser = nil
27
+ replace_parser! IDParser
28
+ @request_queue = []
29
+ end
30
+
31
+ def request
32
+ r = Request.new
33
+ yield r if block_given?
34
+ request_queue << r
35
+ r
36
+ end
37
+
38
+ def replace_parser!(klass)
39
+ new_parser = klass.new(self)
40
+ if @parser
41
+ new_parser.replace(@parser)
42
+ end
43
+ @parser = new_parser
44
+ end
45
+
46
+ def receive_data(dat)
47
+ @parser << dat
48
+ end
49
+
50
+ def receive_id(id)
51
+ # on last line, the messaging can start
52
+ if id.last_one?
53
+ replace_parser! MessageParser
54
+ EM.next_tick do
55
+ ready
56
+ end
57
+ throw :stop
58
+ end
59
+ end
60
+
61
+ # HOOKS TO OVERRIDE, PLEASE CALL SUPER
62
+
63
+ def ready
64
+ end
65
+
66
+ def receive_message_header(head)
67
+ if head.error?
68
+ receive_error_message_header(head)
69
+ elsif head.ok?
70
+ receive_success_message_header(head)
71
+ else
72
+ raise RuntimeError, "nor OK, nor error message #{head}"
73
+ end
74
+ end
75
+
76
+ def receive_error_message_header(head)
77
+ request_queue.shift.error(head)
78
+ end
79
+
80
+ def receive_success_message_header(head)
81
+ request_queue.shift.success(nil) unless head.body?
82
+ end
83
+
84
+ def receive_message(msg)
85
+ request_queue.shift.success(msg)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,14 @@
1
+
2
+ module EM::Rserve
3
+ module Protocol
4
+ ID = Struct.new(:string) do
5
+ def ignorable?
6
+ string == '----' or string == "\r\n\r\n"
7
+ end
8
+
9
+ def last_one?
10
+ string == "--\r\n"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,93 @@
1
+
2
+ require 'em-rserve/protocol/id'
3
+ require 'em-rserve/qap1/header'
4
+ require 'em-rserve/qap1/message'
5
+ require 'em-rserve/r/sexp'
6
+
7
+ module EM::Rserve
8
+ module Protocol
9
+ class Parser
10
+ attr_reader :handler, :buffer
11
+
12
+ def initialize(handler)
13
+ @handler = handler
14
+ @buffer = ''
15
+ end
16
+
17
+ def replace(other)
18
+ @buffer.replace(other.buffer)
19
+ self
20
+ end
21
+
22
+ def << data
23
+ buffer << data if data
24
+ parse_loop!
25
+ end
26
+
27
+ def parse_loop!
28
+ catch :stop do
29
+ loop do
30
+ parse!
31
+ end
32
+ end
33
+ end
34
+
35
+ # should overload this method and throw :stop when more data is needed or any other
36
+ # reason to stop parsing
37
+ def parse!
38
+ raise NotImplementedError, "this class is intended to be a top class, not a useful parser"
39
+ end
40
+ end
41
+
42
+ # This parser is useful only at the beginning.
43
+ # Instead of carrying its dynamic all the time (e.g., keeping a state).
44
+ # We pop-it out as another parser.
45
+ class IDParser < Parser
46
+ def parse!
47
+ if buffer.size >= 4
48
+ dat = buffer.slice(0, 4)
49
+ @buffer = buffer.slice(4 .. -1)
50
+ handler.receive_id(Protocol::ID.new(dat))
51
+ else
52
+ throw :stop
53
+ end
54
+ end
55
+ end
56
+
57
+ # This message parser will parse qap1 headers and associated qap1 data.
58
+ class MessageParser < Parser
59
+ def initialize(handler)
60
+ super(handler)
61
+ @header = nil
62
+ end
63
+
64
+ def parse!
65
+ if @header
66
+ #XXX here we have a header with the size of data
67
+ #to expect, out approach is to delay the message
68
+ #until we have all the data, not well suited for
69
+ #streams but for all other messages this is the
70
+ #good way of doing it
71
+ expected_length = @header.message_length
72
+ if expected_length > 0 and buffer.size >= expected_length
73
+ dat = buffer.slice(0, expected_length)
74
+ @buffer = buffer.slice(expected_length .. -1)
75
+ message = QAP1::Message.from_bin dat
76
+ handler.receive_message message
77
+ @header = nil
78
+ else
79
+ throw :stop
80
+ end
81
+ elsif buffer.size >= 16
82
+ dat = buffer.slice(0, 16)
83
+ @buffer = buffer.slice(16 .. -1)
84
+ @header = QAP1::Header.from_bin dat
85
+ handler.receive_message_header @header
86
+ @header = nil unless @header.body?
87
+ else
88
+ throw :stop
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,22 @@
1
+
2
+ module EM::Rserve
3
+ module Protocol
4
+ Request = Struct.new(:callback_blk, :errback_blk) do
5
+ def callback(&blk)
6
+ self.callback_blk = blk
7
+ end
8
+
9
+ def errback(&blk)
10
+ self.errback_blk = blk
11
+ end
12
+
13
+ def error(val)
14
+ errback_blk.call(val) if errback_blk
15
+ end
16
+
17
+ def success(val)
18
+ callback_blk.call(val) if callback_blk
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,111 @@
1
+
2
+ module EM::Rserve
3
+ module QAP1
4
+ module Constants
5
+ CMD_RESP=0x010000 # all responses have this flag set
6
+ RESP_OK=(CMD_RESP|0x0001) # command succeeded; returned parameters depend on the command issued
7
+ RESP_ERR=(CMD_RESP|0x0002) # command failed, check stats code attached string may describe the error
8
+
9
+ ERR_auth_failed=0x41 # auth.failed or auth.requested but no login came. in case of authentification failure due to name/pwd mismatch, server may send CMD_accessDenied instead
10
+ ERR_conn_broken=0x42 # connection closed or broken packet killed it */
11
+
12
+ ERR_inv_cmd=0x43 # unsupported/invalid command */
13
+ ERR_inv_par=0x44 # some parameters are invalid */
14
+ ERR_Rerror=0x45 # R-error occured, usually followed by connection shutdown */
15
+ ERR_IOerror=0x46 # I/O error */
16
+
17
+
18
+ ERR_notOpen=0x47 # attempt to perform fileRead/Write on closed file */
19
+ ERR_accessDenied=0x48 # this answer is also valid on CMD_login; otherwise it's sent if the server deosn;t allow the user to issue the specified command. (e.g. some server admins may block file I/O operations for some users)
20
+ ERR_unsupportedCmd=0x49 # unsupported command */
21
+ ERR_unknownCmd=0x4a # unknown command - the difference between unsupported and unknown is that unsupported commands are known to the server but for some reasons (e.g. platform dependent) it's not supported. unknown commands are simply not recognized by the server at all. */
22
+ ERR_data_overflow=0x4b # incoming packet is too big. currently there is a limit as of the size of an incoming packet. */
23
+ ERR_object_too_big=0x4c # the requested object is too big to be transported in that way. If received after CMD_eval then the evaluation itself was successful. optional parameter is the size of the object
24
+ ERR_out_of_mem=0x4d # out of memory. the connection is usually closed after this error was sent
25
+ ERR_ctrl_closed=0x4e # control pipe to the master process is closed or broken
26
+ ERR_session_busy=0x50 # session is still busy */
27
+ ERR_detach_failed=0x51 # unable to detach session (cannot determine peer IP or problems creating a listening socket for resume) */
28
+
29
+
30
+ CMD_login=0x001 # "name\npwd" : - */
31
+ CMD_voidEval=0x002 # string : - */
32
+ CMD_eval=0x003 # string : encoded SEXP */
33
+ CMD_shutdown=0x004 # [admin-pwd] : - */
34
+
35
+ #/* file I/O routines. server may answe */
36
+ CMD_openFile=0x010 # fn : - */
37
+ CMD_createFile=0x011 # fn : - */
38
+ CMD_closeFile=0x012 # - : - */
39
+ CMD_readFile=0x013 # [int size] : data... ; if size not present,
40
+ #server is free to choose any value - usually
41
+ #it uses the size of its static buffer */
42
+ CMD_writeFile=0x014 # data : - */
43
+ CMD_removeFile=0x015 # fn : - */
44
+
45
+ # /* object manipulation */
46
+ CMD_setSEXP=0x020 # string(name), REXP : - */
47
+ CMD_assignSEXP=0x021 # string(name), REXP : - ; same as setSEXP except that the name is parsed */
48
+
49
+ # /* session management (since 0.4-0) */
50
+ CMD_detachSession=0x030 # : session key */
51
+ CMD_detachedVoidEval=0x031 # string : session key; doesn't */
52
+ CMD_attachSession=0x032 # session key : - */
53
+
54
+ # control commands (since 0.6-0) - passed on to the master process */
55
+ # Note: currently all control commands are asychronous, i.e. RESP_OK
56
+ # indicates that the command was enqueued in the master pipe, but there
57
+ # is no guarantee that it will be processed. Moreover non-forked
58
+ # connections (e.g. the default debug setup) don't process any
59
+ # control commands until the current client connection is closed so
60
+ # the connection issuing the control command will never see its
61
+ # result.
62
+ CMD_ctrl=0x40 # -- not a command - just a constant -- */
63
+ CMD_ctrlEval=0x42 # string : - */
64
+ CMD_ctrlSource=0x45 # string : - */
65
+ CMD_ctrlShutdown=0x44 # - : - */
66
+
67
+ # /* 'internal' commands (since 0.1-9) */
68
+ CMD_setBufferSize=0x081 # [int sendBufSize] this commad allow clients to request bigger buffer sizes if large data is to be transported from Rserve to the client. (incoming buffer is resized automatically) */
69
+ CMD_setEncoding=0x082 # string (one of "native","latin1","utf8") : -; since 0.5-3 */
70
+
71
+ # /* special commands - the payload of packages with this mask does not contain defined parameters */
72
+
73
+ CMD_SPECIAL_MASK=0xf0
74
+
75
+ CMD_serEval=0xf5 # serialized eval - the packets are raw serialized data without data header */
76
+ CMD_serAssign=0xf6 # serialized assign - serialized list with [[1]]=name, [[2]]=value */
77
+ CMD_serEEval=0xf7 # serialized expression eval - like serEval with one additional evaluation round */
78
+
79
+ # data types for the transport protocol (QAP1)do NOT confuse with XT_.. values.
80
+
81
+ DT_INT=1 # int */
82
+ DT_CHAR=2 # char */
83
+ DT_DOUBLE=3 # double */
84
+ DT_STRING=4 # 0 terminted string */
85
+ DT_BYTESTREAM=5 # stream of bytes (unlike DT_STRING may contain 0) */
86
+ DT_SEXP=10 # encoded SEXP */
87
+ DT_ARRAY=11 # array of objects (i.e. first 4 bytes specify how many subsequent objects are part of the array; 0 is legitimate) */
88
+ DT_LARGE=64 # new in 0102: if this flag is set then the length of the object is coded as 56-bit integer enlarging the header by 4 bytes */
89
+
90
+ ERROR_DESCRIPTIONS={
91
+ ERR_auth_failed=>'auth.failed or auth.requested but no login came',
92
+ ERR_conn_broken=>'connection closed or broken packet killed it',
93
+ ERR_inv_cmd=>"unsupported/invalid command",
94
+ ERR_inv_par=>"some parameters are invalid",
95
+ ERR_Rerror=>"R-error",
96
+ ERR_IOerror=>"I/O error",
97
+ ERR_notOpen=>"attempt to perform fileRead/Write on closed file",
98
+ ERR_accessDenied=>"Access denied",
99
+ ERR_unsupportedCmd=>"unsupported command",
100
+ ERR_unknownCmd=>"unknown command",
101
+ ERR_data_overflow=>"data overflow",
102
+ ERR_object_too_big=>"requested object is too big",
103
+ ERR_out_of_mem=>"out of memory",
104
+ ERR_ctrl_closed=>"control pipe to the master process is closed or broken",
105
+ ERR_session_busy=>"session still busy",
106
+ ERR_detach_failed=>"unable to detach seesion"
107
+ } # error descriptions
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,45 @@
1
+
2
+ require 'em-rserve/qap1/constants'
3
+
4
+ module EM::Rserve
5
+ module QAP1
6
+ Header = Struct.new(:command, :length, :offset, :length2) do
7
+
8
+ def self.from_bin(dat)
9
+ raise unless dat.size == 16
10
+ self.new(* dat.unpack('VVVV'))
11
+ end
12
+
13
+ def message_length
14
+ length #TODO: use length2
15
+ end
16
+
17
+ def body?
18
+ message_length > 0
19
+ end
20
+
21
+ def to_bin
22
+ to_a.pack('VVVV')
23
+ end
24
+
25
+ def response?
26
+ command & Constants::CMD_RESP > 0
27
+ end
28
+
29
+ def ok?
30
+ command & Constants::RESP_OK > 0
31
+ end
32
+
33
+ def error?
34
+ ((command & Constants::RESP_ERR) & ~Constants::RESP_OK) > 0
35
+ end
36
+
37
+ def error
38
+ ((command & ~Constants::RESP_ERR) >> 24) & 0xff
39
+ end
40
+
41
+ # -> self.for_message
42
+ # -> prepare_message
43
+ end
44
+ end
45
+ end