nl 0.2.4 → 0.3.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/nl/async/dispatcher.rb +199 -0
- data/lib/nl/async/driver.rb +39 -0
- data/lib/nl/async/mailbox.rb +83 -0
- data/lib/nl/async/operation.rb +279 -0
- data/lib/nl/async.rb +15 -0
- data/lib/nl/blocking_transport.rb +131 -0
- data/lib/nl/connection.rb +63 -0
- data/lib/nl/core.rb +1 -1
- data/lib/nl/datagram.rb +24 -0
- data/lib/nl/decoder.rb +9 -1
- data/lib/nl/error.rb +10 -0
- data/lib/nl/exchange.rb +102 -0
- data/lib/nl/family.rb +134 -17
- data/lib/nl/genl/connection.rb +89 -0
- data/lib/nl/genl.rb +18 -31
- data/lib/nl/notification.rb +115 -0
- data/lib/nl/notification_router.rb +85 -0
- data/lib/nl/protocols/genl.rb +47 -28
- data/lib/nl/protocols/raw.rb +164 -99
- data/lib/nl/sequence_allocator.rb +36 -0
- data/lib/nl/socket.rb +15 -12
- data/lib/nl/version.rb +1 -1
- data/lib/nl.rb +5 -6
- metadata +20 -5
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative 'datagram'
|
|
4
|
+
require_relative 'exchange'
|
|
5
|
+
require_relative 'notification_router'
|
|
6
|
+
require_relative 'sequence_allocator'
|
|
7
|
+
|
|
8
|
+
module Nl
|
|
9
|
+
# Drives one exchange at a time with blocking socket operations.
|
|
10
|
+
class BlockingTransport
|
|
11
|
+
class ConcurrentOperationError < StandardError; end
|
|
12
|
+
|
|
13
|
+
class UnexpectedSequenceError < StandardError
|
|
14
|
+
attr_reader :expected, :actual
|
|
15
|
+
|
|
16
|
+
def initialize(expected, actual)
|
|
17
|
+
@expected = expected
|
|
18
|
+
@actual = actual
|
|
19
|
+
super("expected Netlink sequence and port ID #{expected.inspect}, got #{actual.inspect}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(socket, notifications:)
|
|
24
|
+
@socket = socket
|
|
25
|
+
@sequences = SequenceAllocator.new
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
@notifications = notifications
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def exchange(protocol, kind, request_class, reply_class, args)
|
|
31
|
+
unless locked = @mutex.try_lock
|
|
32
|
+
raise ConcurrentOperationError, 'BlockingTransport supports only one active operation'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
request = protocol.build_request(kind, request_class, args)
|
|
36
|
+
seq = @sequences.next
|
|
37
|
+
pid = @socket.local_port_id
|
|
38
|
+
key = [seq, pid]
|
|
39
|
+
protocol.send_message(@socket, request, seq:, pid:)
|
|
40
|
+
exchange = Exchange.new(kind:, expects_reply: !reply_class.nil?)
|
|
41
|
+
result = [] unless block_given?
|
|
42
|
+
|
|
43
|
+
until exchange.complete?
|
|
44
|
+
receive(protocol, key, reply_class) do |message|
|
|
45
|
+
case outcome = exchange.accept(message)
|
|
46
|
+
when Exchange::Item
|
|
47
|
+
block_given? ? yield(outcome.value) : result << outcome.value
|
|
48
|
+
when Exchange::Failure
|
|
49
|
+
raise outcome.exception
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return if block_given?
|
|
55
|
+
|
|
56
|
+
kind == :dump ? result : exchange.result
|
|
57
|
+
ensure
|
|
58
|
+
@mutex.unlock if locked
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def async_capable? #: false
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def receive_notification(protocol, timeout: nil)
|
|
66
|
+
channel = @notifications.channel(protocol)
|
|
67
|
+
return channel.pop(timeout: 0)
|
|
68
|
+
rescue TimeoutError
|
|
69
|
+
unless locked = @mutex.try_lock
|
|
70
|
+
raise ConcurrentOperationError, 'BlockingTransport supports only one active operation'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
74
|
+
loop do
|
|
75
|
+
begin
|
|
76
|
+
return channel.pop(timeout: 0)
|
|
77
|
+
rescue TimeoutError
|
|
78
|
+
# Read another datagram below.
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
82
|
+
raise TimeoutError, 'notification receive timed out' if remaining && remaining <= 0
|
|
83
|
+
if remaining && !@socket.wait_readable(remaining)
|
|
84
|
+
raise TimeoutError, 'notification receive timed out'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
receive_notifications
|
|
88
|
+
end
|
|
89
|
+
ensure
|
|
90
|
+
@mutex.unlock if locked
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def close #: void
|
|
94
|
+
@socket.close unless @socket.closed?
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private def receive(protocol, expected_key, reply_class)
|
|
99
|
+
Datagram.each_frame(receive_datagram) do |header, payload|
|
|
100
|
+
actual_key = [header.seq, header.pid]
|
|
101
|
+
if actual_key == expected_key
|
|
102
|
+
yield protocol.decode_frame(header, payload, reply_class)
|
|
103
|
+
elsif header.seq.zero?
|
|
104
|
+
@notifications.route(header, payload)
|
|
105
|
+
else
|
|
106
|
+
raise UnexpectedSequenceError.new(expected_key, actual_key)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
rescue Errno::ENOBUFS
|
|
110
|
+
@notifications.lose_all(NotificationLossError.new('kernel receive buffer overflowed'))
|
|
111
|
+
raise
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private def receive_notifications
|
|
115
|
+
Datagram.each_frame(receive_datagram) do |header, payload|
|
|
116
|
+
if header.seq.zero?
|
|
117
|
+
@notifications.route(header, payload)
|
|
118
|
+
else
|
|
119
|
+
raise UnexpectedSequenceError.new(nil, [header.seq, header.pid])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
rescue Errno::ENOBUFS
|
|
123
|
+
@notifications.lose_all(NotificationLossError.new('kernel receive buffer overflowed'))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private def receive_datagram
|
|
127
|
+
data, = @socket.recvmsg
|
|
128
|
+
IO::Buffer.for(data)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative 'async'
|
|
4
|
+
require_relative 'blocking_transport'
|
|
5
|
+
require_relative 'notification_router'
|
|
6
|
+
require_relative 'socket'
|
|
7
|
+
|
|
8
|
+
module Nl
|
|
9
|
+
# Owns one Netlink socket and the facilities shared by families using it.
|
|
10
|
+
class Connection
|
|
11
|
+
DEFAULT_NOTIFICATION_CAPACITY = 1_024
|
|
12
|
+
|
|
13
|
+
def initialize(protocol:, executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
|
|
14
|
+
socket = Socket.new(protocol.protonum)
|
|
15
|
+
socket.bind(Socket.sockaddr_nl(0, 0))
|
|
16
|
+
notifications = NotificationRouter.new(
|
|
17
|
+
routing: protocol.notification_routing,
|
|
18
|
+
capacity: notification_capacity,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@socket = socket
|
|
22
|
+
@notifications = notifications
|
|
23
|
+
@transport = if executor
|
|
24
|
+
Async::Dispatcher.new(socket, executor:, notifications:)
|
|
25
|
+
else
|
|
26
|
+
BlockingTransport.new(socket, notifications:)
|
|
27
|
+
end
|
|
28
|
+
rescue Exception
|
|
29
|
+
@transport ? @transport.close : socket&.close
|
|
30
|
+
notifications&.close
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def exchange(...) = @transport.exchange(...)
|
|
35
|
+
def exchange_async(...) = @transport.exchange_async(...)
|
|
36
|
+
def async_capable? = @transport.async_capable?
|
|
37
|
+
|
|
38
|
+
def register_notifications(protocol, classes)
|
|
39
|
+
@notifications.register(protocol, classes)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_memberships(group_ids)
|
|
43
|
+
group_ids.each { @socket.add_membership(it) }
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def drop_memberships(group_ids)
|
|
48
|
+
group_ids.each { @socket.drop_membership(it) }
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def receive_notification(protocol, timeout: nil)
|
|
53
|
+
@transport.receive_notification(protocol, timeout:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close
|
|
57
|
+
@transport.close
|
|
58
|
+
nil
|
|
59
|
+
ensure
|
|
60
|
+
@notifications.close
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/nl/core.rb
CHANGED
data/lib/nl/datagram.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative 'core'
|
|
4
|
+
require_relative 'decoder'
|
|
5
|
+
|
|
6
|
+
module Nl
|
|
7
|
+
# Splits Netlink datagrams into aligned header/payload frames.
|
|
8
|
+
module Datagram
|
|
9
|
+
# @rbs (IO::Buffer buffer) { (Core::NlMsgHdr, IO::Buffer) -> void } -> nil
|
|
10
|
+
# | (IO::Buffer buffer) -> Enumerator[[Core::NlMsgHdr, IO::Buffer], nil]
|
|
11
|
+
def self.each_frame(buffer)
|
|
12
|
+
return enum_for(__method__, buffer) unless block_given?
|
|
13
|
+
|
|
14
|
+
decoder = Decoder.new(buffer)
|
|
15
|
+
while decoder.available?(Core::NLMSG_HDRLEN)
|
|
16
|
+
header = Core::NlMsgHdr.decode(decoder)
|
|
17
|
+
payload_size = header.len - Core::NLMSG_HDRLEN
|
|
18
|
+
payload = decoder.get_buffer(payload_size)
|
|
19
|
+
decoder.align_to(Core::NLMSG_ALIGNTO)
|
|
20
|
+
yield header, payload
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/nl/decoder.rb
CHANGED
|
@@ -38,12 +38,20 @@ module Nl
|
|
|
38
38
|
|
|
39
39
|
def get_string(length = @limit - @position)
|
|
40
40
|
nposition = @position + length
|
|
41
|
-
raise OutOfBounds if nposition > @limit
|
|
41
|
+
raise OutOfBounds if length.negative? || nposition > @limit
|
|
42
42
|
value = @buffer.get_string(@position, length)
|
|
43
43
|
@position = nposition
|
|
44
44
|
value
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def get_buffer(length = @limit - @position)
|
|
48
|
+
nposition = @position + length
|
|
49
|
+
raise OutOfBounds if length.negative? || nposition > @limit
|
|
50
|
+
value = @buffer.slice(@position, length)
|
|
51
|
+
@position = nposition
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
def get_zstring(unterminated_ok: false)
|
|
48
56
|
nposition = @position
|
|
49
57
|
nul_found = false
|
data/lib/nl/error.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Nl
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
class ProtocolViolation < Error; end
|
|
4
|
+
class ExhaustedSequenceNumber < Error; end
|
|
5
|
+
class TimeoutError < Error; end
|
|
6
|
+
class ClosedError < Error; end
|
|
7
|
+
class NotificationLossError < Error; end
|
|
8
|
+
class UnknownMulticastGroupError < Error; end
|
|
9
|
+
class UnresolvedMulticastGroupError < Error; end
|
|
10
|
+
end
|
data/lib/nl/exchange.rb
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require_relative 'protocols/raw'
|
|
5
|
+
|
|
6
|
+
module Nl
|
|
7
|
+
# State machine for one Netlink request/reply exchange.
|
|
8
|
+
class Exchange
|
|
9
|
+
Item = Data.define(:value)
|
|
10
|
+
Complete = Data.define
|
|
11
|
+
COMPLETE = Complete.new
|
|
12
|
+
Failure = Data.define(:exception)
|
|
13
|
+
|
|
14
|
+
attr_reader :kind
|
|
15
|
+
|
|
16
|
+
def initialize(kind:, expects_reply:)
|
|
17
|
+
@kind = kind
|
|
18
|
+
@expects_reply = expects_reply
|
|
19
|
+
@reply = nil
|
|
20
|
+
@state = kind == :dump ? :multi : :initial
|
|
21
|
+
@acked = false
|
|
22
|
+
@cancelled = false
|
|
23
|
+
@complete = false
|
|
24
|
+
@result = nil
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def accept(frame)
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
return if @complete
|
|
31
|
+
|
|
32
|
+
case frame
|
|
33
|
+
when Protocols::Raw::UnknownFrame
|
|
34
|
+
nil
|
|
35
|
+
when Protocols::Raw::ErrorFrame
|
|
36
|
+
@cancelled ? complete(nil) : fail_with(SystemCallError.new(frame.errno))
|
|
37
|
+
when Protocols::Raw::DoneFrame
|
|
38
|
+
if frame.errno && !@cancelled
|
|
39
|
+
fail_with(SystemCallError.new(frame.errno))
|
|
40
|
+
else
|
|
41
|
+
complete(@reply)
|
|
42
|
+
end
|
|
43
|
+
when Protocols::Raw::AckFrame
|
|
44
|
+
accept_ack
|
|
45
|
+
when Protocols::Raw::DataFrame
|
|
46
|
+
accept_reply(frame)
|
|
47
|
+
else
|
|
48
|
+
raise ArgumentError, "unexpected exchange input: #{frame.inspect}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cancel
|
|
54
|
+
@mutex.synchronize { @cancelled = true unless @complete }
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cancelled? = @mutex.synchronize { @cancelled }
|
|
59
|
+
def complete? = @mutex.synchronize { @complete }
|
|
60
|
+
def result = @mutex.synchronize { @result }
|
|
61
|
+
def expects_reply? = @expects_reply
|
|
62
|
+
|
|
63
|
+
private def accept_ack
|
|
64
|
+
@acked = true
|
|
65
|
+
if @cancelled || !@expects_reply || @state == :single
|
|
66
|
+
complete(@reply)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def accept_reply(frame)
|
|
71
|
+
return if @cancelled
|
|
72
|
+
|
|
73
|
+
multipart = (frame.header.flags.to_i & Core::NLM_F_MULTI) != 0
|
|
74
|
+
case @state
|
|
75
|
+
when :initial
|
|
76
|
+
@state = multipart ? :multi : :single
|
|
77
|
+
when :single
|
|
78
|
+
return fail_with(ProtocolViolation.new('more than one data message in a non-multipart Netlink response'))
|
|
79
|
+
when :multi
|
|
80
|
+
unless multipart
|
|
81
|
+
return fail_with(ProtocolViolation.new('multipart Netlink response contains data without NLM_F_MULTI'))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return Item.new(frame.message) if @kind == :dump
|
|
86
|
+
|
|
87
|
+
@reply ||= frame.message
|
|
88
|
+
complete(frame.message) if @acked && @state == :single
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private def complete(value)
|
|
92
|
+
@complete = true
|
|
93
|
+
@result = value
|
|
94
|
+
COMPLETE
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def fail_with(exception)
|
|
98
|
+
@complete = true
|
|
99
|
+
Failure.new(exception)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/nl/family.rb
CHANGED
|
@@ -1,37 +1,154 @@
|
|
|
1
1
|
#--
|
|
2
2
|
# rbs_inline: enabled
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'connection'
|
|
4
|
+
require_relative 'notification'
|
|
4
5
|
|
|
5
6
|
module Nl
|
|
7
|
+
# @rbs!
|
|
8
|
+
# type executor = :thread | :fiber
|
|
9
|
+
#
|
|
10
|
+
# interface _Connection
|
|
11
|
+
# def exchange: (
|
|
12
|
+
# Protocols::Raw protocol,
|
|
13
|
+
# Symbol kind,
|
|
14
|
+
# Class request_class,
|
|
15
|
+
# Class reply_class,
|
|
16
|
+
# Hash[Symbol, untyped] args
|
|
17
|
+
# ) ?{ (untyped) -> void } -> untyped
|
|
18
|
+
# def exchange_async: (
|
|
19
|
+
# Protocols::Raw protocol,
|
|
20
|
+
# Symbol kind,
|
|
21
|
+
# Class request_class,
|
|
22
|
+
# Class reply_class,
|
|
23
|
+
# Hash[Symbol, untyped] args,
|
|
24
|
+
# ?stream_capacity: Integer?
|
|
25
|
+
# ) -> (Async::Future[untyped] | Async::Stream[untyped])
|
|
26
|
+
# def async_capable?: () -> bool
|
|
27
|
+
# def register_notifications: (Protocols::Raw, Hash[Integer, Class]) -> NotificationChannel
|
|
28
|
+
# def add_memberships: (Array[Integer]) -> nil
|
|
29
|
+
# def drop_memberships: (Array[Integer]) -> nil
|
|
30
|
+
# def receive_notification: (Protocols::Raw, ?timeout: Numeric?) -> untyped
|
|
31
|
+
# def close: () -> nil
|
|
32
|
+
# end
|
|
33
|
+
|
|
6
34
|
class Family
|
|
35
|
+
DEFAULT_NOTIFICATION_CAPACITY = Connection::DEFAULT_NOTIFICATION_CAPACITY
|
|
36
|
+
|
|
37
|
+
module Session
|
|
38
|
+
def close #: nil
|
|
39
|
+
@connection.close
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
7
43
|
#--
|
|
8
|
-
# @rbs
|
|
44
|
+
# @rbs connection: _Connection
|
|
9
45
|
# @rbs protocol: Protocol
|
|
10
46
|
# @rbs return: instance
|
|
11
|
-
def initialize(
|
|
12
|
-
@socket = socket
|
|
47
|
+
def initialize(connection, protocol: self.class::PROTOCOL)
|
|
13
48
|
@protocol = protocol
|
|
49
|
+
@connection = connection
|
|
50
|
+
@connection.register_notifications(@protocol, notification_classes)
|
|
51
|
+
@notification_stream = NotificationStream.new do |timeout|
|
|
52
|
+
@connection.receive_notification(@protocol, timeout:)
|
|
53
|
+
end
|
|
14
54
|
end
|
|
15
55
|
|
|
16
56
|
#--
|
|
17
|
-
# @rbs () -> instance
|
|
18
|
-
# | [R] () { (instance) -> R } -> R
|
|
19
|
-
def self.open
|
|
57
|
+
# @rbs (?executor: executor?, ?notification_capacity: Integer?) -> (Session & instance)
|
|
58
|
+
# | [R] (?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
59
|
+
def self.open(executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
|
|
60
|
+
session = build_session(executor:, notification_capacity:)
|
|
61
|
+
return session unless block_given?
|
|
62
|
+
|
|
20
63
|
begin
|
|
21
|
-
|
|
22
|
-
socket.bind(Socket.sockaddr_nl(0, 0))
|
|
23
|
-
if block_given?
|
|
24
|
-
yield new(socket)
|
|
25
|
-
else
|
|
26
|
-
return new(socket)
|
|
27
|
-
end
|
|
64
|
+
yield session
|
|
28
65
|
ensure
|
|
29
|
-
|
|
66
|
+
session.close
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# @rbs (?executor: executor?, notification_capacity: Integer) -> (Session & instance)
|
|
72
|
+
private def build_session(executor: nil, notification_capacity:)
|
|
73
|
+
protocol = self::PROTOCOL
|
|
74
|
+
connection = Connection.new(protocol:, executor:, notification_capacity:)
|
|
75
|
+
new(connection).extend(Session)
|
|
76
|
+
rescue Exception
|
|
77
|
+
connection&.close
|
|
78
|
+
raise
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def exchange_message(kind, request_class, reply_class, args, &block)
|
|
83
|
+
@connection.exchange(@protocol, kind, request_class, reply_class, args, &block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def async_capable? #: bool
|
|
87
|
+
@connection.async_capable?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Adds multicast memberships to the existing family socket. Membership is
|
|
91
|
+
# additive and remains active until explicitly removed or the owner closes.
|
|
92
|
+
def subscribe(*groups)
|
|
93
|
+
@connection.add_memberships(multicast_group_ids(groups))
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unsubscribe(*groups)
|
|
98
|
+
@connection.drop_memberships(multicast_group_ids(groups))
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def receive_notification(timeout: nil)
|
|
103
|
+
@notification_stream.next(timeout:)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def each_notification(&block)
|
|
107
|
+
return @notification_stream.each unless block
|
|
108
|
+
|
|
109
|
+
@notification_stream.each(&block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Builds a generated asynchronous-operation facade with a narrowly scoped
|
|
113
|
+
# callback, so the facade does not need access to Family's private API.
|
|
114
|
+
#--
|
|
115
|
+
# @rbs operations_class: Class
|
|
116
|
+
# @rbs stream_capacity: Integer?
|
|
117
|
+
# @rbs return: untyped
|
|
118
|
+
private def build_async_facade(operations_class, stream_capacity: nil)
|
|
119
|
+
unless async_capable?
|
|
120
|
+
raise Async::UnavailableError, 'async operations require an executor'
|
|
30
121
|
end
|
|
122
|
+
|
|
123
|
+
operations_class.new do |kind, request_class, reply_class, args|
|
|
124
|
+
exchange_message_async(kind, request_class, reply_class, args, stream_capacity:)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private def exchange_message_async(kind, request_class, reply_class, args, stream_capacity: nil)
|
|
129
|
+
@connection.exchange_async(@protocol, kind, request_class, reply_class, args, stream_capacity:)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def notification_classes
|
|
133
|
+
self.class.const_get(:NOTIFICATIONS, false)
|
|
134
|
+
rescue NameError
|
|
135
|
+
{}
|
|
31
136
|
end
|
|
32
137
|
|
|
33
|
-
private def
|
|
34
|
-
|
|
138
|
+
private def multicast_groups
|
|
139
|
+
self.class.const_get(:MCAST_GROUPS, false)
|
|
140
|
+
rescue NameError
|
|
141
|
+
{}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private def multicast_group_ids(names)
|
|
145
|
+
names.map do |name|
|
|
146
|
+
key = name.to_sym
|
|
147
|
+
group = multicast_groups.fetch(key) do
|
|
148
|
+
raise UnknownMulticastGroupError, "unknown multicast group #{name.inspect} for #{@protocol.name}"
|
|
149
|
+
end
|
|
150
|
+
@protocol.multicast_group_id(group.name, group.id)
|
|
151
|
+
end
|
|
35
152
|
end
|
|
36
153
|
end
|
|
37
154
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Generic Netlink connection handling
|
|
2
|
+
#--
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative '../connection'
|
|
6
|
+
require_relative '../core'
|
|
7
|
+
require_relative '../protocols/genl'
|
|
8
|
+
|
|
9
|
+
module Nl
|
|
10
|
+
module Genl
|
|
11
|
+
FamilyInfo = Data.define(:id, :multicast_groups) do
|
|
12
|
+
def self.from_reply(reply)
|
|
13
|
+
groups = (reply.mcast_groups || []).to_h do |attributes|
|
|
14
|
+
[attributes[:name].value.to_sym, attributes[:id].value]
|
|
15
|
+
end
|
|
16
|
+
new(id: reply.family_id, multicast_groups: groups.freeze)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Connection
|
|
21
|
+
#--
|
|
22
|
+
# @rbs (resolver: ^(instance, ::String) -> (Integer | FamilyInfo | untyped), ?executor: executor?, ?notification_capacity: Integer?) -> instance
|
|
23
|
+
# | [R] (resolver: ^(instance, ::String) -> (Integer | FamilyInfo | untyped), ?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
24
|
+
def self.open(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
25
|
+
conn = new(resolver:, executor:, notification_capacity:)
|
|
26
|
+
if block_given?
|
|
27
|
+
begin
|
|
28
|
+
yield conn
|
|
29
|
+
ensure
|
|
30
|
+
conn.close
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
conn
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#--
|
|
38
|
+
# @rbs resolver: ^(instance, ::String) -> (Integer | FamilyInfo | untyped)
|
|
39
|
+
# @rbs executor: executor?
|
|
40
|
+
# @rbs return: instance
|
|
41
|
+
def initialize(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
42
|
+
@resolver = resolver
|
|
43
|
+
@family_cache = {}
|
|
44
|
+
@family_cache_mutex = Mutex.new
|
|
45
|
+
@connection = Nl::Connection.new(
|
|
46
|
+
protocol: Protocols::Genl,
|
|
47
|
+
executor:,
|
|
48
|
+
notification_capacity:,
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def family(family_class)
|
|
53
|
+
proto = family_class::PROTOCOL
|
|
54
|
+
info = family_info(proto)
|
|
55
|
+
family_class.new(
|
|
56
|
+
@connection,
|
|
57
|
+
protocol: Protocols::Genl.new(
|
|
58
|
+
proto.name,
|
|
59
|
+
family_id: info.id,
|
|
60
|
+
multicast_groups: info.multicast_groups,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def close
|
|
66
|
+
@connection.close
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private def family_info(protocol)
|
|
70
|
+
id = protocol.family_id
|
|
71
|
+
FamilyInfo.new(id:, multicast_groups: {}.freeze)
|
|
72
|
+
rescue NotImplementedError
|
|
73
|
+
cached_info = @family_cache_mutex.synchronize { @family_cache[protocol.name] }
|
|
74
|
+
return cached_info if cached_info
|
|
75
|
+
|
|
76
|
+
resolved = @resolver.call(self, protocol.name)
|
|
77
|
+
info = case resolved
|
|
78
|
+
when Integer
|
|
79
|
+
FamilyInfo.new(id: resolved, multicast_groups: {}.freeze)
|
|
80
|
+
when FamilyInfo
|
|
81
|
+
resolved
|
|
82
|
+
else
|
|
83
|
+
FamilyInfo.from_reply(resolved)
|
|
84
|
+
end
|
|
85
|
+
@family_cache_mutex.synchronize { @family_cache[protocol.name] ||= info }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/nl/genl.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Generic Netlink wire definitions
|
|
2
|
+
#--
|
|
3
|
+
# rbs_inline: enabled
|
|
2
4
|
|
|
3
5
|
require_relative 'core'
|
|
4
6
|
require_relative 'endian'
|
|
@@ -48,40 +50,25 @@ module Nl
|
|
|
48
50
|
end
|
|
49
51
|
include Constants
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
else
|
|
61
|
-
conn
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def initialize(resolver:)
|
|
66
|
-
@socket = Socket.new(Core::NETLINK_GENERIC)
|
|
67
|
-
@socket.bind(Socket.sockaddr_nl(0, 0))
|
|
68
|
-
@resolver = resolver
|
|
69
|
-
@id_cache = {}
|
|
70
|
-
end
|
|
53
|
+
GenlMsgHdr = Struct.new(:cmd, :version, :reserved)
|
|
54
|
+
# Generic Netlink message header
|
|
55
|
+
class GenlMsgHdr
|
|
56
|
+
FORMAT = Ractor.make_shareable([
|
|
57
|
+
Endian::Host::U8,
|
|
58
|
+
Endian::Host::U8,
|
|
59
|
+
Endian::Host::U16,
|
|
60
|
+
])
|
|
61
|
+
private_constant :FORMAT
|
|
71
62
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
id = @id_cache[proto.name] ||= begin
|
|
75
|
-
proto.family_id
|
|
76
|
-
rescue NotImplementedError
|
|
77
|
-
@resolver.call(@socket, proto.name)
|
|
78
|
-
end
|
|
79
|
-
family_class.new(@socket, protocol: Protocols::Genl.new(proto.name, family_id: id))
|
|
63
|
+
def self.decode(decoder)
|
|
64
|
+
new(*decoder.get_values(FORMAT))
|
|
80
65
|
end
|
|
81
66
|
|
|
82
|
-
def
|
|
83
|
-
|
|
67
|
+
def encode(encoder)
|
|
68
|
+
encoder.put_values(FORMAT, to_a)
|
|
84
69
|
end
|
|
85
70
|
end
|
|
86
71
|
end
|
|
87
72
|
end
|
|
73
|
+
|
|
74
|
+
require_relative 'genl/connection'
|