ciri 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+
24
+ require 'logger'
25
+
26
+ module Ciri
27
+ module DevP2P
28
+
29
+ # simple actor model implementation
30
+ # Example:
31
+ #
32
+ # class Hello
33
+ # include Actor
34
+ #
35
+ # def say_hello
36
+ # puts 'hello world'
37
+ # 'hello world'
38
+ # end
39
+ # end
40
+ #
41
+ # actor = Hello.new
42
+ # # start actor loop
43
+ # actor.start
44
+ # # push message to actor inbox
45
+ # actor << :say_hello
46
+ # # push message and wait until get response
47
+ # actor.call(:say_hello).value
48
+ #
49
+ # # raise error
50
+ # actor.call(:hello).value # NoMethodError
51
+ #
52
+ # # stop actor
53
+ # actor.send_stop
54
+ # actor.wait
55
+ #
56
+ module Actor
57
+
58
+ LOGGER = Logger.new(STDERR, datetime_format: '%Y-%m-%d %H:%M:%S', level: Logger::INFO)
59
+
60
+ # future, use this to wait actor msg respond
61
+ class Future
62
+ def initialize
63
+ @value = nil
64
+ @done = false
65
+ end
66
+
67
+ def value=(val)
68
+ if @done
69
+ raise RuntimeError.new('future value duplicated set')
70
+ end
71
+ @done = true
72
+ @queue << :done if @queue
73
+ @value = val
74
+ end
75
+
76
+ def value
77
+ loop do
78
+ if @done
79
+ return @value
80
+ elsif @error
81
+ raise @error
82
+ else
83
+ queue.pop
84
+ end
85
+ end
86
+ end
87
+
88
+ def raise_error(error)
89
+ error.set_backtrace(caller) if error.backtrace.nil?
90
+ @error = error
91
+ @queue << :error if @queue
92
+ end
93
+
94
+ private
95
+ def queue
96
+ @queue ||= Queue.new
97
+ end
98
+ end
99
+
100
+ class Error < StandardError
101
+ end
102
+
103
+ # stop actor
104
+ class StopError < Error
105
+ end
106
+
107
+ class StateError < Error
108
+ end
109
+
110
+ attr_accessor :executor
111
+
112
+ def initialize(executor: nil)
113
+ @inbox = Queue.new
114
+ @executor = executor
115
+ @future = Future.new
116
+ @running = false
117
+ end
118
+
119
+ # async call
120
+ def enqueue(method, *args)
121
+ self << [method, *args]
122
+ end
123
+
124
+ def <<(args)
125
+ @inbox << args
126
+ end
127
+
128
+ # sync call, push msg to inbox, and return future
129
+ #
130
+ # Example:
131
+ # future = actor.call(:result) # future
132
+ # future.value # blocking and wait for result
133
+ #
134
+ def call(method, *args)
135
+ future = Future.new
136
+ self << [future, method, *args]
137
+ future
138
+ end
139
+
140
+ # start actor
141
+ def start
142
+ raise Error.new("must set executor before start") unless executor
143
+
144
+ @running = true
145
+ executor.post do
146
+ start_loop
147
+ end
148
+ end
149
+
150
+ # send stop to actor
151
+ #
152
+ # Example:
153
+ # actor.send_stop
154
+ # # wait for actor actually stopped
155
+ # actor.wait
156
+ #
157
+ def send_stop
158
+ self << [:raise_error, StopError.new]
159
+ end
160
+
161
+ # wait until an error occurs
162
+ def wait
163
+ raise StateError.new('actor not running!') unless @running
164
+ @future.value
165
+ end
166
+
167
+ # start loop
168
+ def start_loop
169
+ loop_callback do |wait_message: true|
170
+ # check inbox
171
+ next Thread.pass if @inbox.empty? && !wait_message
172
+ msg = @inbox.pop
173
+
174
+ # extract sync or async call
175
+ future = nil
176
+ method, *args = msg
177
+ if method.is_a?(Future)
178
+ future = method
179
+ method, *args = args
180
+ end
181
+ begin
182
+ val = send(method, *args)
183
+ rescue StandardError => e
184
+ future.raise_error(e) if future
185
+ raise
186
+ end
187
+ # if future not nil, set value
188
+ future.value = val if future
189
+ end while true
190
+
191
+ rescue StopError
192
+ # actor stop
193
+ @future.value = nil
194
+ rescue StandardError => e
195
+ @future.raise_error e
196
+ LOGGER.error("Actor #{self}") {"#{e}\n#{e.backtrace.join("\n")}"}
197
+ ensure
198
+ @running = false
199
+ end
200
+
201
+ # allow inject callback into actor loop
202
+ # Example:
203
+ #
204
+ # class A
205
+ # include Actor
206
+ #
207
+ # def loop_callback
208
+ # # before handle msg
209
+ # yield
210
+ # # after handle msg
211
+ # end
212
+ # end
213
+ #
214
+ def loop_callback
215
+ yield
216
+ end
217
+
218
+ def raise_error(e)
219
+ raise e
220
+ end
221
+ end
222
+
223
+ end
224
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ require_relative 'rlpx'
26
+ require_relative 'actor'
27
+ require_relative 'protocol_io'
28
+
29
+ module Ciri
30
+ module DevP2P
31
+
32
+ # represent a connected remote node
33
+ class Peer
34
+
35
+ class DiscoverError < StandardError
36
+ end
37
+ class UnknownMessageCodeError < StandardError
38
+ end
39
+
40
+ include Actor
41
+
42
+ attr_reader :connection
43
+
44
+ def initialize(connection, handshake, protocols)
45
+ @connection = connection
46
+ @handshake = handshake
47
+ @protocols = protocols
48
+ @protocol_io_hash = make_protocol_io_hash(protocols, handshake.caps, connection)
49
+ super()
50
+ end
51
+
52
+ def node_id
53
+ @node_id ||= RLPX::NodeID.from_raw_id(@handshake.id)
54
+ end
55
+
56
+ # start peer
57
+ # handle msg, handle sub protocols
58
+ def start
59
+ executor.post {read_loop}
60
+ start_protocols
61
+ super
62
+ end
63
+
64
+ # read and handle msg
65
+ def read_loop
66
+ loop do
67
+ msg = connection.read_msg
68
+ msg.received_at = Time.now
69
+ handle(msg)
70
+ end
71
+ rescue StandardError => e
72
+ self << [:raise_error, e]
73
+ end
74
+
75
+ def start_protocols
76
+ @protocols.each do |protocol|
77
+ protocol.start.(self, @protocol_io_hash[protocol.name])
78
+ end
79
+ end
80
+
81
+ def handle(msg)
82
+ if msg.code == RLPX::MESSAGES[:ping]
83
+ #TODO send pong
84
+ elsif msg.code == RLPX::MESSAGES[:discover]
85
+ reason = RLP.decode_with_type(msg.payload, Integer)
86
+ raise DiscoverError.new("receive error discovery message, reason: #{reason}")
87
+ else
88
+ # send msg to sub protocol
89
+ if (protocol_io = find_protocol_io_by_msg_code(msg.code)).nil?
90
+ raise UnknownMessageCodeError.new("can't find protocol with msg code #{msg.code}")
91
+ end
92
+ protocol_io.msg_queue << msg
93
+ end
94
+ end
95
+
96
+ private
97
+ def find_protocol_io_by_msg_code(code)
98
+ @protocol_io_hash.values.find do |protocol_io|
99
+ offset = protocol_io.offset
100
+ protocol = protocol_io.protocol
101
+ code >= offset && code < offset + protocol.length
102
+ end
103
+ end
104
+
105
+ # return protocol_io_hash
106
+ # handle multiple sub protocols upon one io
107
+ def make_protocol_io_hash(protocols, caps, io)
108
+ # sub protocol offset
109
+ offset = RLPX::BASE_PROTOCOL_LENGTH
110
+ result = {}
111
+ # [name, version] as key
112
+ protocols_hash = protocols.map {|protocol| [[protocol.name, protocol.version], protocol]}.to_h
113
+ sorted_caps = caps.sort_by {|c| [c.name, c.version]}
114
+
115
+ sorted_caps.each do |cap|
116
+ protocol = protocols_hash[[cap.name, cap.version]]
117
+ next unless protocol
118
+ # ignore same name old protocols
119
+ if (old = result[cap.name])
120
+ result.delete(cap.name)
121
+ offset -= old.protocol.length
122
+ end
123
+ result[cap.name] = ProtocolIO.new(protocol, offset, io)
124
+ # move offset, to support next protocol
125
+ offset += protocol.length
126
+ end
127
+ result
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ require_relative 'actor'
26
+
27
+ module Ciri
28
+ module DevP2P
29
+
30
+ # protocol represent DevP2P sub protocols
31
+ class Protocol
32
+
33
+ attr_reader :name, :version, :length
34
+ attr_accessor :node_info, :peer_info, :start
35
+
36
+ def initialize(name:, version:, length:)
37
+ @name = name
38
+ @version = version
39
+ @length = length
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+
24
+ require_relative 'actor'
25
+ require_relative 'rlpx/message'
26
+
27
+ module Ciri
28
+ module DevP2P
29
+
30
+ # send/read sub protocol msg
31
+ class ProtocolIO
32
+
33
+ class Error < StandardError
34
+ end
35
+ class InvalidMessageCode < Error
36
+ end
37
+
38
+ attr_reader :protocol, :offset, :msg_queue
39
+
40
+ def initialize(protocol, offset, frame_io)
41
+ @protocol = protocol
42
+ @offset = offset
43
+ @frame_io = frame_io
44
+ @msg_queue = Queue.new
45
+ end
46
+
47
+ def send_data(code, data)
48
+ msg = RLPX::Message.new(code: code, size: data.size, payload: data)
49
+ write_msg(msg)
50
+ end
51
+
52
+ def write_msg(msg)
53
+ raise InvalidMessageCode, "code #{code} must less than length #{protocol.length}" if msg.code > protocol.length
54
+ msg.code += offset
55
+ @frame_io.write_msg(msg)
56
+ end
57
+
58
+ def read_msg
59
+ msg = msg_queue.pop
60
+ msg.code -= offset
61
+ msg
62
+ end
63
+ end
64
+
65
+ end
66
+ end