mfp 0.1.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.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/lib/mfp/client.rb +390 -0
- data/lib/mfp/compressor.rb +35 -0
- data/lib/mfp/enum.rb +83 -0
- data/lib/mfp/error.rb +20 -0
- data/lib/mfp/event.rb +25 -0
- data/lib/mfp/outgoing_message.rb +15 -0
- data/lib/mfp/ping_handler.rb +70 -0
- data/lib/mfp/proto/error.rb +52 -0
- data/lib/mfp/proto/frame.rb +53 -0
- data/lib/mfp/proto/hello.rb +49 -0
- data/lib/mfp/proto/hello_response.rb +43 -0
- data/lib/mfp/proto/ping.rb +33 -0
- data/lib/mfp/proto/protocol.rb +96 -0
- data/lib/mfp/proto/reader.rb +26 -0
- data/lib/mfp/proto/writer.rb +28 -0
- data/lib/mfp/proto.rb +10 -0
- data/lib/mfp/resettable_timer.rb +47 -0
- data/lib/mfp/server/conn.rb +362 -0
- data/lib/mfp/server/server.rb +118 -0
- data/lib/mfp/server.rb +4 -0
- data/lib/mfp/tls_config.rb +12 -0
- data/lib/mfp/version.rb +5 -0
- data/lib/mfp.rb +39 -0
- metadata +143 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
class PingHandler
|
|
5
|
+
def initialize(idle_timeout:, ping_timeout:)
|
|
6
|
+
@timer_notify = Async::Queue.new
|
|
7
|
+
@notifications = Async::Queue.new
|
|
8
|
+
|
|
9
|
+
@idle_timer = ResettableTimer.new(idle_timeout, @timer_notify, tag: :idle)
|
|
10
|
+
@response_timer = ResettableTimer.new(ping_timeout, @timer_notify, tag: :response)
|
|
11
|
+
|
|
12
|
+
@waiting_response = false
|
|
13
|
+
@waiting_cookie = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def recv = @notifications.dequeue
|
|
17
|
+
|
|
18
|
+
def start(task)
|
|
19
|
+
task.async { @idle_timer.start(task) }
|
|
20
|
+
task.async { @response_timer.start(task) }
|
|
21
|
+
task.async do
|
|
22
|
+
loop do
|
|
23
|
+
ev, tag = @timer_notify.dequeue
|
|
24
|
+
if ev.nil? && tag.nil?
|
|
25
|
+
break
|
|
26
|
+
elsif tag == :response && ev == :timeout
|
|
27
|
+
@notifications.enqueue({ kind: :timeout })
|
|
28
|
+
stop
|
|
29
|
+
break
|
|
30
|
+
elsif tag == :idle && ev == :timeout
|
|
31
|
+
next if @waiting_response
|
|
32
|
+
|
|
33
|
+
@waiting_response = true
|
|
34
|
+
@waiting_cookie = SecureRandom.bytes(8).unpack1("Q>")
|
|
35
|
+
@notifications.enqueue({
|
|
36
|
+
kind: :request,
|
|
37
|
+
cookie: @waiting_cookie,
|
|
38
|
+
})
|
|
39
|
+
@response_timer.reset
|
|
40
|
+
else
|
|
41
|
+
raise "Unexpected ev/tag pair: #{ev}, #{tag}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def notify_received
|
|
48
|
+
return if @waiting_response
|
|
49
|
+
|
|
50
|
+
@idle_timer.reset
|
|
51
|
+
@response_timer.reset
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def notify_pong(cookie)
|
|
55
|
+
return if !@waiting_response || cookie != @waiting_cookie
|
|
56
|
+
|
|
57
|
+
@waiting_response = false
|
|
58
|
+
@waiting_cookie = nil
|
|
59
|
+
@idle_timer.reset
|
|
60
|
+
@response_timer.reset
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stop
|
|
64
|
+
@idle_timer.stop
|
|
65
|
+
@response_timer.stop
|
|
66
|
+
@timer_notify.close
|
|
67
|
+
@notifications.close
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
# Error is the payload of a MessageKindError frame. It signals a problem on
|
|
6
|
+
# either a stream or the connection. The enclosing frame's StreamID
|
|
7
|
+
# disambiguate the scope: zero for connection-level errors, non-zero for
|
|
8
|
+
# stream-level errors.
|
|
9
|
+
class Error
|
|
10
|
+
attr_accessor :code, :terminal, :debug
|
|
11
|
+
|
|
12
|
+
def initialize(code, terminal: false, debug: nil)
|
|
13
|
+
@code = code
|
|
14
|
+
@terminal = terminal
|
|
15
|
+
@debug = debug
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encode(stream_id)
|
|
19
|
+
buf = Writer.new
|
|
20
|
+
buf.write_u8(@code)
|
|
21
|
+
if (size = @debug&.bytesize || 0).positive?
|
|
22
|
+
buf.write_u32(size)
|
|
23
|
+
buf.write(@debug)
|
|
24
|
+
else
|
|
25
|
+
buf.write_u32(0)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Frame.new(
|
|
29
|
+
call_sign: nil,
|
|
30
|
+
stream_id:,
|
|
31
|
+
kind: MessageKind::ERROR,
|
|
32
|
+
flags: @terminal ? 1 : 0,
|
|
33
|
+
length: buf.length,
|
|
34
|
+
payload: buf.string,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.read(fr)
|
|
39
|
+
raise ParserError.new(ErrorKind::FRAME_SIZE_ERROR) if fr.length < 5
|
|
40
|
+
|
|
41
|
+
reader = Reader.from_bytes(fr.payload)
|
|
42
|
+
terminal = fr.flags.allbits?(0x01)
|
|
43
|
+
code = reader.read_byte
|
|
44
|
+
debug_len = reader.read_u32
|
|
45
|
+
debug = nil
|
|
46
|
+
debug = reader.read_n(debug_len) if debug_len.positive?
|
|
47
|
+
|
|
48
|
+
new(code, terminal:, debug:)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
# Frame is the fundamental unit of the MFP wire protocol. Every message
|
|
6
|
+
# exchanged between endpoints is wrapped in a Frame.
|
|
7
|
+
class Frame
|
|
8
|
+
attr_accessor :call_sign, :stream_id, :kind, :flags, :length, :payload
|
|
9
|
+
|
|
10
|
+
def initialize(stream_id:, kind:, flags:, length:, payload:, call_sign: "")
|
|
11
|
+
@call_sign = call_sign
|
|
12
|
+
@stream_id = stream_id
|
|
13
|
+
@kind = kind
|
|
14
|
+
@flags = flags
|
|
15
|
+
@length = length
|
|
16
|
+
@payload = payload
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def encode
|
|
20
|
+
w = Writer.new
|
|
21
|
+
w.write(@call_sign)
|
|
22
|
+
w.write_u32(@stream_id || 0)
|
|
23
|
+
w.write_u8(@kind)
|
|
24
|
+
w.write_u8(@flags || 0)
|
|
25
|
+
w.write_u32(@length || 0)
|
|
26
|
+
w.write(@payload || "")
|
|
27
|
+
w.string
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.read(io, max_len, call_sign)
|
|
31
|
+
r = Reader.new(io)
|
|
32
|
+
cs = r.read_n(4)
|
|
33
|
+
if cs.nil?
|
|
34
|
+
# This is a clean disconnect before reading the callsign.
|
|
35
|
+
# Raise ConnectionClosedError instead of an spurious
|
|
36
|
+
# InvalidCallSignError
|
|
37
|
+
raise ConnectionClosedError
|
|
38
|
+
end
|
|
39
|
+
raise InvalidCallSignError, cs.inspect if cs != call_sign
|
|
40
|
+
|
|
41
|
+
stream_id = r.read_u32
|
|
42
|
+
kind = r.read_byte
|
|
43
|
+
kind = MessageKind::UNKNOWN if kind > MessageKind.max
|
|
44
|
+
flags = r.read_byte
|
|
45
|
+
length = r.read_u32
|
|
46
|
+
raise PayloadTooLargeError if max_len.positive? && length > max_len
|
|
47
|
+
|
|
48
|
+
payload = length.positive? ? r.read_n(length) : ""
|
|
49
|
+
new(call_sign:, stream_id:, kind:, flags:, length:, payload:)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
# Hello is the payload of a MessageKindHello frame. It must be the first
|
|
6
|
+
# frame sent by the client on every connection. The server responds with
|
|
7
|
+
# either a HelloResponse or an Error.
|
|
8
|
+
class Hello
|
|
9
|
+
attr_accessor :version_major, :version_minor, :capabilities, :application, :host
|
|
10
|
+
|
|
11
|
+
def initialize(version_major:, version_minor:, capabilities: 0, application: "", host: "")
|
|
12
|
+
@version_major = version_major
|
|
13
|
+
@version_minor = version_minor
|
|
14
|
+
@capabilities = capabilities
|
|
15
|
+
@application = application
|
|
16
|
+
@host = host
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def encode(stream_id)
|
|
20
|
+
buf = Writer.new
|
|
21
|
+
buf.write_u8(@version_major)
|
|
22
|
+
buf.write_u8(@version_minor)
|
|
23
|
+
buf.write_u16(@capabilities)
|
|
24
|
+
buf.write_u16_string(@application)
|
|
25
|
+
buf.write_u16_string(@host)
|
|
26
|
+
|
|
27
|
+
Frame.new(
|
|
28
|
+
stream_id:,
|
|
29
|
+
kind: MessageKind::HELLO,
|
|
30
|
+
flags: 0,
|
|
31
|
+
length: buf.length,
|
|
32
|
+
payload: buf.string,
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.read(fr)
|
|
37
|
+
raise ParserError.new(ErrorKind::FRAME_SIZE_ERROR) if fr.length < 8
|
|
38
|
+
|
|
39
|
+
r = Reader.new(StringIO.new(fr.payload))
|
|
40
|
+
version_major = r.read_byte
|
|
41
|
+
version_minor = r.read_byte
|
|
42
|
+
capabilities = r.read_u16
|
|
43
|
+
application = r.read_u16_string
|
|
44
|
+
host = r.read_u16_string
|
|
45
|
+
new(version_major:, version_minor:, capabilities:, application:, host:)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
# HelloResponse is the payload of a MessageKindHelloResponse frame, sent by
|
|
6
|
+
# the server upon successfully accepting a Hello. It confirms the negotiated
|
|
7
|
+
# capabilities and advertises connection limits.
|
|
8
|
+
class HelloResponse
|
|
9
|
+
attr_accessor :capabilities, :max_streams, :max_payload
|
|
10
|
+
|
|
11
|
+
def initialize(capabilities:, max_streams:, max_payload:)
|
|
12
|
+
@capabilities = capabilities
|
|
13
|
+
@max_streams = max_streams
|
|
14
|
+
@max_payload = max_payload
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def encode(stream_id)
|
|
18
|
+
buf = Writer.new
|
|
19
|
+
buf.write_u16(@capabilities)
|
|
20
|
+
buf.write_u32(@max_streams)
|
|
21
|
+
buf.write_u32(@max_payload)
|
|
22
|
+
Frame.new(
|
|
23
|
+
stream_id:,
|
|
24
|
+
kind: MessageKind::HELLO_RESPONSE,
|
|
25
|
+
flags: 0,
|
|
26
|
+
length: buf.length,
|
|
27
|
+
payload: buf.string,
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.read(fr)
|
|
32
|
+
raise ParserError.new(ErrorKind::FRAME_SIZE_ERROR) if fr.length != 10
|
|
33
|
+
|
|
34
|
+
r = Reader.new(StringIO.new(fr.payload))
|
|
35
|
+
capabilities = r.read_u16
|
|
36
|
+
max_streams = r.read_u32
|
|
37
|
+
max_payload = r.read_u32
|
|
38
|
+
|
|
39
|
+
new(capabilities:, max_streams:, max_payload:)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
class Ping
|
|
6
|
+
attr_accessor :ack, :payload
|
|
7
|
+
|
|
8
|
+
def initialize(ack:, payload:)
|
|
9
|
+
@ack = ack
|
|
10
|
+
@payload = payload
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def encode(*)
|
|
14
|
+
buf = Writer.new
|
|
15
|
+
buf.write_u64(@payload)
|
|
16
|
+
Frame.new(
|
|
17
|
+
stream_id: 0,
|
|
18
|
+
kind: MessageKind::PING,
|
|
19
|
+
flags: @ack ? 0x01 : 0x00,
|
|
20
|
+
length: 8,
|
|
21
|
+
payload: buf.string,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.read(fr)
|
|
26
|
+
new(
|
|
27
|
+
ack: fr.flags.allbits?(0x01),
|
|
28
|
+
payload: fr.payload.unpack1("Q>"),
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
# Message Kind -------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
class MessageKind < Enum
|
|
8
|
+
# UNKNOWN is the zero value and represents an unrecognised or
|
|
9
|
+
# uninitialised frame kind. Frames with this kind MUST be rejected as a
|
|
10
|
+
# protocol violation.
|
|
11
|
+
define :UNKNOWN, 0
|
|
12
|
+
|
|
13
|
+
# HELLO is sent by the client as the first frame of every
|
|
14
|
+
# connection. No other frame may be sent before the handshake is complete
|
|
15
|
+
define :HELLO, 1
|
|
16
|
+
|
|
17
|
+
# HELLO_RESPONSE is sent by the server in response to a Hello
|
|
18
|
+
# frame, confirming negotiated capabilities and connection limits
|
|
19
|
+
define :HELLO_RESPONSE, 2
|
|
20
|
+
|
|
21
|
+
# PING may be sent by either endpoint to verify that the
|
|
22
|
+
# connection is still alive. The receiving endpoint must echo the payload
|
|
23
|
+
# with the ACK flag set.
|
|
24
|
+
define :PING, 3
|
|
25
|
+
|
|
26
|
+
# ERROR signals a problem on a stream or the connection itself.
|
|
27
|
+
# The frame's StreamID disambiguates scope: zero for connection-level
|
|
28
|
+
# errors, non-zero for stream-level errors.
|
|
29
|
+
define :ERROR, 4
|
|
30
|
+
|
|
31
|
+
# STREAM carries an opaque payload for a given stream. The
|
|
32
|
+
# frame's StreamID must be non-zero. Payload semantics are defined by
|
|
33
|
+
# the upper-layer protocol.
|
|
34
|
+
define :STREAM, 5
|
|
35
|
+
|
|
36
|
+
# CONTROL carries an opaque, out-of-band signal scoped to
|
|
37
|
+
# either a stream (non-zero StreamID) or the connection (zero StreamID).
|
|
38
|
+
# Control frames are exempt from compression. Payload semantics are
|
|
39
|
+
# defined by the upper-layer protocol.
|
|
40
|
+
define :CONTROL, 6
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Capabilities -------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
class Capability < Enum
|
|
46
|
+
# COMPRESSION_GZIP indicates support for gzip payload
|
|
47
|
+
# compression on STREAM frames.
|
|
48
|
+
define :COMPRESSION_GZIP, 1 << 0
|
|
49
|
+
# COMPRESSION_BROTLI indicates support for Brotli payload
|
|
50
|
+
# compression on STREAM frames.
|
|
51
|
+
define :COMPRESSION_BROTLI, 1 << 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Error Kinds --------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
class ErrorKind < Enum
|
|
57
|
+
# PROTOCOL_ERROR signals that an invalid frame was received at this point
|
|
58
|
+
# in the stream's or connection's lifetime. Applicable to both streams
|
|
59
|
+
# and connections.
|
|
60
|
+
define :PROTOCOL_ERROR, 0x01
|
|
61
|
+
|
|
62
|
+
# INTERNAL_ERROR signals that an unexpected internal error occurred.
|
|
63
|
+
# Applicable to both streams and connections.
|
|
64
|
+
define :INTERNAL_ERROR, 0x02
|
|
65
|
+
|
|
66
|
+
# STREAM_CLOSED_ERROR signals that the targeted stream has been terminated
|
|
67
|
+
# or was never established. Applicable to streams only.
|
|
68
|
+
define :STREAM_CLOSED_ERROR, 0x03
|
|
69
|
+
|
|
70
|
+
# REFUSED_STREAM_ERROR signals that the server refused to open the stream.
|
|
71
|
+
# Applicable to streams only.
|
|
72
|
+
define :REFUSED_STREAM_ERROR, 0x04
|
|
73
|
+
|
|
74
|
+
# FRAME_SIZE_ERROR signals that a frame exceeded the negotiated Max. Payload
|
|
75
|
+
# or had an otherwise invalid size (e.g. a PING whose Length ≠ 8).
|
|
76
|
+
# Applicable to both streams and connections.
|
|
77
|
+
define :FRAME_SIZE_ERROR, 0x05
|
|
78
|
+
|
|
79
|
+
# COMPRESSION_ERROR signals that a payload could not be decompressed.
|
|
80
|
+
# Applicable to connections only.
|
|
81
|
+
define :COMPRESSION_ERROR, 0x06
|
|
82
|
+
|
|
83
|
+
# ENHANCE_YOUR_CALM_ERROR signals that the peer is generating excessive load.
|
|
84
|
+
# Applicable to connections only.
|
|
85
|
+
define :ENHANCE_YOUR_CALM_ERROR, 0x07
|
|
86
|
+
|
|
87
|
+
# INADEQUATE_SECURITY_ERROR signals that the underlying transport does not
|
|
88
|
+
# meet minimum security requirements. Applicable to connections only.
|
|
89
|
+
define :INADEQUATE_SECURITY_ERROR, 0x08
|
|
90
|
+
|
|
91
|
+
# INSUFFICIENT_CAPACITY_ERROR signals that the server lacks capacity to
|
|
92
|
+
# handle the request. Applicable to connections only.
|
|
93
|
+
define :INSUFFICIENT_CAPACITY_ERROR, 0x09
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
class Reader
|
|
6
|
+
def self.from_bytes(value) = new(StringIO.new(value))
|
|
7
|
+
|
|
8
|
+
def initialize(io)
|
|
9
|
+
@io = io
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def read_n(n) = @io.read(n)
|
|
13
|
+
def read_u16 = @io.read(2).unpack1("S>")
|
|
14
|
+
def read_u32 = @io.read(4).unpack1("L>")
|
|
15
|
+
def read_u64 = @io.read(8).unpack1("Q>")
|
|
16
|
+
def read_byte = @io.read(1).unpack1("C")
|
|
17
|
+
|
|
18
|
+
def read_u16_string
|
|
19
|
+
size = read_u16
|
|
20
|
+
return "" if size.zero?
|
|
21
|
+
|
|
22
|
+
read_n(size).encode("utf-8")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Proto
|
|
5
|
+
class Writer
|
|
6
|
+
def initialize
|
|
7
|
+
@buf = StringIO.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write(value) = @buf.write(value)
|
|
11
|
+
def write_u8(value) = write([value].pack("C"))
|
|
12
|
+
def write_u16(value) = write([value].pack("S>"))
|
|
13
|
+
def write_u32(value) = write([value].pack("L>"))
|
|
14
|
+
def write_u64(value) = write([value].pack("Q>"))
|
|
15
|
+
|
|
16
|
+
def write_u16_string(value)
|
|
17
|
+
l = value&.bytesize || 0
|
|
18
|
+
write_u16(l)
|
|
19
|
+
return if l.zero?
|
|
20
|
+
|
|
21
|
+
write(value.encode("utf-8"))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def string = @buf.string
|
|
25
|
+
def length = @buf.length
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/mfp/proto.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "proto/protocol"
|
|
4
|
+
require_relative "proto/writer"
|
|
5
|
+
require_relative "proto/reader"
|
|
6
|
+
require_relative "proto/frame"
|
|
7
|
+
require_relative "proto/error"
|
|
8
|
+
require_relative "proto/hello"
|
|
9
|
+
require_relative "proto/hello_response"
|
|
10
|
+
require_relative "proto/ping"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
class ResettableTimer
|
|
5
|
+
def initialize(timeout, notify_queue, tag: nil)
|
|
6
|
+
@timeout = timeout
|
|
7
|
+
@reset_condition = Async::Condition.new
|
|
8
|
+
@running = true
|
|
9
|
+
@notify_queue = notify_queue
|
|
10
|
+
@tag = tag
|
|
11
|
+
@itask = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start(task)
|
|
15
|
+
@itask = task.async do |task|
|
|
16
|
+
while @running
|
|
17
|
+
result = Async::Queue.new
|
|
18
|
+
|
|
19
|
+
sleeper = task.async do |s|
|
|
20
|
+
s.sleep(@timeout)
|
|
21
|
+
result.enqueue(:timeout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
waiter = task.async do
|
|
25
|
+
@reset_condition.wait
|
|
26
|
+
result.enqueue(:reset)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
msg = result.dequeue
|
|
30
|
+
sleeper.stop unless sleeper.stopped?
|
|
31
|
+
waiter.stop unless waiter.stopped?
|
|
32
|
+
next if msg == :reset
|
|
33
|
+
|
|
34
|
+
@notify_queue.enqueue [msg, @tag]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset = @reset_condition.signal
|
|
40
|
+
|
|
41
|
+
def stop
|
|
42
|
+
@itask&.stop
|
|
43
|
+
@running = false
|
|
44
|
+
@reset_condition.signal
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|