nl 0.2.4 → 0.4.1
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 +21 -0
- data/Rakefile +7 -0
- data/lib/nl/async/dispatcher.rb +199 -0
- data/lib/nl/async/driver.rb +37 -0
- data/lib/nl/async/mailbox.rb +81 -0
- data/lib/nl/async/operation.rb +277 -0
- data/lib/nl/async.rb +13 -0
- data/lib/nl/attribute_set.rb +173 -0
- data/lib/nl/bitfield32.rb +86 -0
- data/lib/nl/blocking_transport.rb +131 -0
- data/lib/nl/connection.rb +61 -0
- data/lib/nl/datagram.rb +22 -0
- data/lib/nl/datatypes.rb +454 -0
- data/lib/nl/decoder.rb +13 -1
- data/lib/nl/endian.rb +10 -0
- data/lib/nl/error.rb +10 -0
- data/lib/nl/exchange.rb +124 -0
- data/lib/nl/family.rb +117 -23
- data/lib/nl/genl/client.rb +104 -0
- data/lib/nl/genl/protocol.rb +59 -0
- data/lib/nl/genl/wire.rb +94 -0
- data/lib/nl/genl.rb +60 -73
- data/lib/nl/notification.rb +112 -0
- data/lib/nl/notification_router.rb +92 -0
- data/lib/nl/raw/client.rb +83 -0
- data/lib/nl/raw/protocol.rb +112 -0
- data/lib/nl/raw/wire.rb +146 -0
- data/lib/nl/raw.rb +104 -0
- data/lib/nl/sequence_allocator.rb +34 -0
- data/lib/nl/socket.rb +15 -12
- data/lib/nl/structured_payload.rb +61 -0
- data/lib/nl/sub_message.rb +94 -0
- data/lib/nl/version.rb +1 -1
- data/lib/nl.rb +10 -12
- data/sig/generated/nl/async/dispatcher.rbs +53 -0
- data/sig/generated/nl/async/driver.rbs +21 -0
- data/sig/generated/nl/async/mailbox.rbs +32 -0
- data/sig/generated/nl/async/operation.rbs +123 -0
- data/sig/generated/nl/async.rbs +9 -0
- data/sig/generated/nl/attribute_set.rbs +50 -0
- data/sig/generated/nl/bitfield32.rbs +41 -0
- data/sig/generated/nl/blocking_transport.rbs +33 -0
- data/sig/generated/nl/connection.rbs +26 -0
- data/sig/generated/nl/datagram.rbs +11 -0
- data/sig/generated/nl/datatypes.rbs +181 -0
- data/sig/generated/nl/decoder.rbs +38 -0
- data/sig/generated/nl/encoder.rbs +29 -0
- data/sig/generated/nl/endian.rbs +28 -0
- data/sig/generated/nl/error.rbs +27 -0
- data/sig/generated/nl/exchange.rbs +75 -0
- data/sig/generated/nl/family.rbs +71 -0
- data/sig/generated/nl/genl/client.rbs +74 -0
- data/sig/generated/nl/genl/protocol.rbs +37 -0
- data/sig/generated/nl/genl/wire.rbs +113 -0
- data/sig/generated/nl/genl.rbs +50 -0
- data/sig/generated/nl/notification.rbs +53 -0
- data/sig/generated/nl/notification_router.rbs +30 -0
- data/sig/generated/nl/raw/client.rbs +50 -0
- data/sig/generated/nl/raw/protocol.rbs +122 -0
- data/sig/generated/nl/raw/wire.rbs +151 -0
- data/sig/generated/nl/raw.rbs +70 -0
- data/sig/generated/nl/sequence_allocator.rbs +15 -0
- data/sig/generated/nl/socket.rbs +40 -0
- data/sig/generated/nl/structured_payload.rbs +25 -0
- data/sig/generated/nl/sub_message.rbs +74 -0
- data/sig/generated/nl/version.rbs +5 -0
- data/sig/generated/nl.rbs +2 -0
- metadata +65 -9
- data/lib/nl/core.rb +0 -94
- data/lib/nl/protocols/genl.rb +0 -55
- data/lib/nl/protocols/raw.rb +0 -395
data/lib/nl/family.rb
CHANGED
|
@@ -1,37 +1,131 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require_relative 'socket'
|
|
1
|
+
require_relative 'connection'
|
|
2
|
+
require_relative 'notification'
|
|
4
3
|
|
|
5
4
|
module Nl
|
|
5
|
+
# @rbs!
|
|
6
|
+
# type executor = :thread | :fiber
|
|
7
|
+
#
|
|
8
|
+
# interface _Connection
|
|
9
|
+
# def exchange: (
|
|
10
|
+
# Raw::Endpoint endpoint,
|
|
11
|
+
# Symbol kind,
|
|
12
|
+
# Class request_class,
|
|
13
|
+
# Class reply_class,
|
|
14
|
+
# Hash[Symbol, untyped] args
|
|
15
|
+
# ) ?{ (untyped) -> void } -> untyped
|
|
16
|
+
# def exchange_async: (
|
|
17
|
+
# Raw::Endpoint endpoint,
|
|
18
|
+
# Symbol kind,
|
|
19
|
+
# Class request_class,
|
|
20
|
+
# Class reply_class,
|
|
21
|
+
# Hash[Symbol, untyped] args,
|
|
22
|
+
# ?stream_capacity: Integer?
|
|
23
|
+
# ) -> (Async::Future[untyped] | Async::Stream[untyped])
|
|
24
|
+
# def async_capable?: () -> bool
|
|
25
|
+
# def register_notifications: (Raw::Endpoint, Hash[Integer, Class]) -> NotificationChannel
|
|
26
|
+
# def add_memberships: (Array[Integer]) -> nil
|
|
27
|
+
# def drop_memberships: (Array[Integer]) -> nil
|
|
28
|
+
# def receive_notification: (Raw::Endpoint, ?timeout: Numeric?) -> untyped
|
|
29
|
+
# def close: () -> nil
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# interface _FamilyClass[out F]
|
|
33
|
+
# def new: (_Connection, endpoint: Raw::Endpoint) -> F
|
|
34
|
+
# end
|
|
35
|
+
|
|
6
36
|
class Family
|
|
37
|
+
DEFAULT_NOTIFICATION_CAPACITY = Connection::DEFAULT_NOTIFICATION_CAPACITY
|
|
38
|
+
|
|
39
|
+
module Session
|
|
40
|
+
def close #: nil
|
|
41
|
+
@connection.close
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
7
45
|
#--
|
|
8
|
-
# @rbs
|
|
9
|
-
# @rbs
|
|
46
|
+
# @rbs connection: _Connection
|
|
47
|
+
# @rbs endpoint: Raw::Endpoint
|
|
10
48
|
# @rbs return: instance
|
|
11
|
-
def initialize(
|
|
12
|
-
@
|
|
13
|
-
@
|
|
49
|
+
def initialize(connection, endpoint:)
|
|
50
|
+
@endpoint = endpoint
|
|
51
|
+
@connection = connection
|
|
52
|
+
@connection.register_notifications(@endpoint, notification_classes)
|
|
53
|
+
@notification_stream = NotificationStream.new do |timeout|
|
|
54
|
+
@connection.receive_notification(@endpoint, timeout:)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private def exchange_message(kind, request_class, reply_class, args, &block)
|
|
59
|
+
@connection.exchange(@endpoint, kind, request_class, reply_class, args, &block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def async_capable? #: bool
|
|
63
|
+
@connection.async_capable?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Adds multicast memberships to the existing family socket. Membership is
|
|
67
|
+
# additive and remains active until explicitly removed or the owner closes.
|
|
68
|
+
def subscribe(*groups)
|
|
69
|
+
@connection.add_memberships(multicast_group_ids(groups))
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unsubscribe(*groups)
|
|
74
|
+
@connection.drop_memberships(multicast_group_ids(groups))
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def receive_notification(timeout: nil)
|
|
79
|
+
@notification_stream.next(timeout:)
|
|
14
80
|
end
|
|
15
81
|
|
|
82
|
+
def each_notification(&block)
|
|
83
|
+
return @notification_stream.each unless block
|
|
84
|
+
|
|
85
|
+
@notification_stream.each(&block)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Builds a generated asynchronous-operation facade with a narrowly scoped
|
|
89
|
+
# callback, so the facade does not need access to Family's private API.
|
|
16
90
|
#--
|
|
17
|
-
# @rbs
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if block_given?
|
|
24
|
-
yield new(socket)
|
|
25
|
-
else
|
|
26
|
-
return new(socket)
|
|
27
|
-
end
|
|
28
|
-
ensure
|
|
29
|
-
socket&.close if block_given?
|
|
91
|
+
# @rbs operations_class: Class
|
|
92
|
+
# @rbs stream_capacity: Integer?
|
|
93
|
+
# @rbs return: untyped
|
|
94
|
+
private def build_async_facade(operations_class, stream_capacity: nil)
|
|
95
|
+
unless async_capable?
|
|
96
|
+
raise Async::UnavailableError, 'async operations require an executor'
|
|
30
97
|
end
|
|
98
|
+
|
|
99
|
+
operations_class.new do |kind, request_class, reply_class, args|
|
|
100
|
+
exchange_message_async(kind, request_class, reply_class, args, stream_capacity:)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private def exchange_message_async(kind, request_class, reply_class, args, stream_capacity: nil)
|
|
105
|
+
@connection.exchange_async(@endpoint, kind, request_class, reply_class, args, stream_capacity:)
|
|
31
106
|
end
|
|
32
107
|
|
|
33
|
-
private def
|
|
34
|
-
|
|
108
|
+
private def notification_classes
|
|
109
|
+
self.class.const_get(:NOTIFICATIONS, false)
|
|
110
|
+
rescue NameError
|
|
111
|
+
{}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private def multicast_groups
|
|
115
|
+
self.class.const_get(:MCAST_GROUPS, false)
|
|
116
|
+
rescue NameError
|
|
117
|
+
{}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private def multicast_group_ids(names)
|
|
121
|
+
names.map do |name|
|
|
122
|
+
key = name.to_sym
|
|
123
|
+
group = multicast_groups.fetch(key) do
|
|
124
|
+
raise UnknownMulticastGroupError, "unknown multicast group #{name.inspect} for #{@endpoint.name}"
|
|
125
|
+
end
|
|
126
|
+
@endpoint.multicast_group_id(group.name, group.id)
|
|
127
|
+
end
|
|
35
128
|
end
|
|
36
129
|
end
|
|
130
|
+
|
|
37
131
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Generic Netlink client handling
|
|
2
|
+
|
|
3
|
+
require_relative '../connection'
|
|
4
|
+
require_relative 'wire'
|
|
5
|
+
|
|
6
|
+
module Nl
|
|
7
|
+
module Genl
|
|
8
|
+
# Dynamically resolved information for a generic Netlink family.
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [r] id
|
|
11
|
+
# @return [Integer] the assigned family ID
|
|
12
|
+
# @!attribute [r] multicast_groups
|
|
13
|
+
# @return [Hash<String, Integer>] multicast group names mapped to their IDs
|
|
14
|
+
FamilyInfo = Data.define(
|
|
15
|
+
:id, #: Integer
|
|
16
|
+
:multicast_groups, #: Hash[String, Integer]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Owns one generic Netlink connection shared by compatible families.
|
|
20
|
+
class Client
|
|
21
|
+
# Opens a generic Netlink client.
|
|
22
|
+
#
|
|
23
|
+
# @overload open(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
24
|
+
# The caller is responsible for closing the client.
|
|
25
|
+
# @param [#call] resolver a callable that resolves a family name to {FamilyInfo}
|
|
26
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
27
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
28
|
+
# @return [Client] the opened client
|
|
29
|
+
# @overload open(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY, &block)
|
|
30
|
+
# The client is automatically closed after the block returns.
|
|
31
|
+
# @param [#call] resolver a callable that resolves a family name to {FamilyInfo}
|
|
32
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
33
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
34
|
+
# @yieldparam [Client] client the opened client
|
|
35
|
+
# @return [Object] the value returned from the block
|
|
36
|
+
# @rbs (resolver: ^(instance, ::String) -> FamilyInfo, ?executor: executor?, ?notification_capacity: Integer?) -> instance
|
|
37
|
+
# | [R] (resolver: ^(instance, ::String) -> FamilyInfo, ?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
38
|
+
def self.open(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
39
|
+
client = new(resolver:, executor:, notification_capacity:)
|
|
40
|
+
return client unless block_given?
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
yield client
|
|
44
|
+
ensure
|
|
45
|
+
client.close
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param [#call] resolver a callable that resolves a family name to {FamilyInfo}
|
|
50
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
51
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
52
|
+
# @rbs (resolver: ^(instance, ::String) -> FamilyInfo, ?executor: executor?, ?notification_capacity: Integer?) -> void
|
|
53
|
+
def initialize(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
54
|
+
@resolver = resolver
|
|
55
|
+
@family_cache = {}
|
|
56
|
+
@family_cache_mutex = Mutex.new
|
|
57
|
+
@connection = Nl::Connection.new(
|
|
58
|
+
protocol: Protocol.new,
|
|
59
|
+
executor:,
|
|
60
|
+
notification_capacity:,
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Builds a family backed by this client's connection.
|
|
65
|
+
#
|
|
66
|
+
# @param [Class<Family>] family_class a generic Netlink family class
|
|
67
|
+
# @return [Family] an instance of `family_class`
|
|
68
|
+
# @raise [TypeError] if +family_class+ does not inherit from {Family}
|
|
69
|
+
# @rbs [F < Family] (_FamilyClass[F] family_class) -> F
|
|
70
|
+
def family(family_class)
|
|
71
|
+
unless family_class <= Family
|
|
72
|
+
raise TypeError, "family class must inherit from #{Family}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
info = family_info(family_class::NAME)
|
|
76
|
+
family_class.new(
|
|
77
|
+
@connection,
|
|
78
|
+
endpoint: Endpoint.new(family_class, info),
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Closes the underlying Netlink connection.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
# @rbs () -> void
|
|
86
|
+
def close
|
|
87
|
+
@connection.close
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Resolves and caches information for a family by name.
|
|
91
|
+
#
|
|
92
|
+
# @param [String] name the generic Netlink family name
|
|
93
|
+
# @return [FamilyInfo] the resolved family information
|
|
94
|
+
# @rbs (String name) -> FamilyInfo
|
|
95
|
+
private def family_info(name)
|
|
96
|
+
cached_info = @family_cache_mutex.synchronize { @family_cache[name] }
|
|
97
|
+
return cached_info if cached_info
|
|
98
|
+
|
|
99
|
+
info = @resolver.call(self, name)
|
|
100
|
+
@family_cache_mutex.synchronize { @family_cache[name] ||= info }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative 'wire'
|
|
2
|
+
require_relative '../raw'
|
|
3
|
+
|
|
4
|
+
module Nl
|
|
5
|
+
module Genl
|
|
6
|
+
# A generated Generic Netlink family bound to nlctrl-provided information.
|
|
7
|
+
class Endpoint < Raw::Endpoint
|
|
8
|
+
attr_reader :info
|
|
9
|
+
|
|
10
|
+
def initialize(definition, info)
|
|
11
|
+
super(definition)
|
|
12
|
+
@info = info
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def family_id = @info.id
|
|
16
|
+
def version = @definition.version
|
|
17
|
+
def frame_type(_message_class) = family_id
|
|
18
|
+
|
|
19
|
+
def multicast_group_id(name, _value)
|
|
20
|
+
@info.multicast_groups.fetch(name) do
|
|
21
|
+
raise UnresolvedMulticastGroupError,
|
|
22
|
+
"Generic Netlink multicast group #{name.inspect} was not resolved"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Socket-wide Generic Netlink wire behavior.
|
|
28
|
+
class Protocol < Raw::Protocol
|
|
29
|
+
def initialize
|
|
30
|
+
super(Raw::NETLINK_GENERIC)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def encode_message(encoder, endpoint, request, seq:, pid:)
|
|
34
|
+
message = request.message
|
|
35
|
+
header = Raw::NlMsgHdr.new(0, request.type, request.flags, seq, pid)
|
|
36
|
+
encoder.measure(Endian::Host::U16) do
|
|
37
|
+
header.encode(encoder)
|
|
38
|
+
Nl::Genl::GenlMsgHdr.new(message.class::TYPE, endpoint.version, 0).encode(encoder)
|
|
39
|
+
message.encode(encoder)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def notification_channel_key(endpoint) = endpoint.family_id
|
|
44
|
+
def notification_route_keys(endpoint, _classes) = [endpoint.family_id]
|
|
45
|
+
def notification_frame_key(header) = header.type
|
|
46
|
+
|
|
47
|
+
def notification_frame?(endpoint, header, _payload)
|
|
48
|
+
header.type == endpoint.family_id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def notification_class(endpoint, header, payload, classes)
|
|
52
|
+
return unless notification_frame?(endpoint, header, payload)
|
|
53
|
+
|
|
54
|
+
command = Nl::Genl::GenlMsgHdr.decode(Decoder.new(payload)).cmd
|
|
55
|
+
classes[command]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/nl/genl/wire.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Generic Netlink wire definitions
|
|
2
|
+
|
|
3
|
+
require_relative '../raw/wire'
|
|
4
|
+
require_relative '../endian'
|
|
5
|
+
|
|
6
|
+
module Nl
|
|
7
|
+
module Genl
|
|
8
|
+
# Constants from <linux/genetlink.h>
|
|
9
|
+
module Constants
|
|
10
|
+
GENL_NAMSIZ = 16
|
|
11
|
+
GENL_MIN_ID = Raw::NLMSG_MIN_TYPE
|
|
12
|
+
GENL_MAX_ID = 1023
|
|
13
|
+
|
|
14
|
+
GENL_HDRLEN = 4
|
|
15
|
+
|
|
16
|
+
GENL_ID_GENERATE = 0
|
|
17
|
+
GENL_ID_CTRL = Raw::NLMSG_MIN_TYPE
|
|
18
|
+
GENL_ID_VFS_DQUOT = Raw::NLMSG_MIN_TYPE + 1
|
|
19
|
+
GENL_ID_PMCRAID = Raw::NLMSG_MIN_TYPE + 2
|
|
20
|
+
|
|
21
|
+
CTRL_CMD_UNSPEC = 0
|
|
22
|
+
CTRL_CMD_NEWFAMILY = 1
|
|
23
|
+
CTRL_CMD_DELFAMILY = 2
|
|
24
|
+
CTRL_CMD_GETFAMILY = 3
|
|
25
|
+
CTRL_CMD_NEWOPS = 4
|
|
26
|
+
CTRL_CMD_DELOPS = 5
|
|
27
|
+
CTRL_CMD_GETOPS = 6
|
|
28
|
+
CTRL_CMD_NEWMCAST_GRP = 7
|
|
29
|
+
CTRL_CMD_DELMCAST_GRP = 8
|
|
30
|
+
CTRL_CMD_GETMCAST_GRP = 9
|
|
31
|
+
|
|
32
|
+
CTRL_ATTR_UNSPEC = 0
|
|
33
|
+
CTRL_ATTR_FAMILY_ID = 1
|
|
34
|
+
CTRL_ATTR_FAMILY_NAME = 2
|
|
35
|
+
CTRL_ATTR_VERSION = 3
|
|
36
|
+
CTRL_ATTR_HDRSIZE = 4
|
|
37
|
+
CTRL_ATTR_MAXATTR = 5
|
|
38
|
+
CTRL_ATTR_OPS = 6
|
|
39
|
+
CTRL_ATTR_MCAST_GROUPS = 7
|
|
40
|
+
|
|
41
|
+
CTRL_ATTR_OP_UNSPEC = 0
|
|
42
|
+
CTRL_ATTR_OP_ID = 1
|
|
43
|
+
CTRL_ATTR_OP_FLAGS = 2
|
|
44
|
+
|
|
45
|
+
CTRL_ATTR_MCAST_GRP_UNSPEC = 0
|
|
46
|
+
CTRL_ATTR_MCAST_GRP_NAME = 1
|
|
47
|
+
CTRL_ATTR_MCAST_GRP_ID = 2
|
|
48
|
+
end
|
|
49
|
+
include Constants
|
|
50
|
+
|
|
51
|
+
# Header prepended to a Generic Netlink payload after the Netlink header.
|
|
52
|
+
#
|
|
53
|
+
# This corresponds to Linux's +struct genlmsghdr+.
|
|
54
|
+
#
|
|
55
|
+
# @!attribute [rw] cmd
|
|
56
|
+
# @return [Integer] family-specific command identifier
|
|
57
|
+
# @!attribute [rw] version
|
|
58
|
+
# @return [Integer] family-specific protocol version
|
|
59
|
+
# @!attribute [rw] reserved
|
|
60
|
+
# @return [Integer] reserved field, which must be zero
|
|
61
|
+
GenlMsgHdr = Struct.new(
|
|
62
|
+
:cmd, #: Integer
|
|
63
|
+
:version, #: Integer
|
|
64
|
+
:reserved, #: Integer
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
class GenlMsgHdr
|
|
68
|
+
FORMAT = Ractor.make_shareable([
|
|
69
|
+
Endian::Host::U8,
|
|
70
|
+
Endian::Host::U8,
|
|
71
|
+
Endian::Host::U16,
|
|
72
|
+
])
|
|
73
|
+
private_constant :FORMAT
|
|
74
|
+
|
|
75
|
+
# Decodes a header from the decoder's current position.
|
|
76
|
+
#
|
|
77
|
+
# @param [Decoder] decoder the source decoder
|
|
78
|
+
# @return [GenlMsgHdr] the decoded header
|
|
79
|
+
# @rbs (Decoder decoder) -> instance
|
|
80
|
+
def self.decode(decoder)
|
|
81
|
+
new(*decoder.get_values(FORMAT))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Encodes this header at the encoder's current position.
|
|
85
|
+
#
|
|
86
|
+
# @param [Encoder] encoder the destination encoder
|
|
87
|
+
# @return [void]
|
|
88
|
+
# @rbs (Encoder encoder) -> void
|
|
89
|
+
def encode(encoder)
|
|
90
|
+
encoder.put_values(FORMAT, to_a)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/nl/genl.rb
CHANGED
|
@@ -1,87 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Generic Netlink family support
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative '
|
|
3
|
+
require_relative 'genl/wire'
|
|
4
|
+
require_relative 'family'
|
|
5
|
+
require_relative 'raw'
|
|
6
|
+
require_relative 'genl/protocol'
|
|
5
7
|
|
|
6
8
|
module Nl
|
|
9
|
+
# Generic Netlink families.
|
|
7
10
|
module Genl
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
# Base class for Generic Netlink families.
|
|
12
|
+
class Family < Nl::Family
|
|
13
|
+
# Returns the family-specific protocol version.
|
|
14
|
+
#
|
|
15
|
+
# @return [Integer] the protocol version
|
|
16
|
+
# @rbs () -> Integer
|
|
17
|
+
def self.version = self::VERSION
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
CTRL_ATTR_OPS = 6
|
|
39
|
-
CTRL_ATTR_MCAST_GROUPS = 7
|
|
40
|
-
|
|
41
|
-
CTRL_ATTR_OP_UNSPEC = 0
|
|
42
|
-
CTRL_ATTR_OP_ID = 1
|
|
43
|
-
CTRL_ATTR_OP_FLAGS = 2
|
|
44
|
-
|
|
45
|
-
CTRL_ATTR_MCAST_GRP_UNSPEC = 0
|
|
46
|
-
CTRL_ATTR_MCAST_GRP_NAME = 1
|
|
47
|
-
CTRL_ATTR_MCAST_GRP_ID = 2
|
|
48
|
-
end
|
|
49
|
-
include Constants
|
|
50
|
-
|
|
51
|
-
class Connection
|
|
52
|
-
def self.open(resolver:)
|
|
53
|
-
conn = new(resolver:)
|
|
54
|
-
if block_given?
|
|
55
|
-
begin
|
|
56
|
-
yield conn
|
|
57
|
-
ensure
|
|
58
|
-
conn.close
|
|
59
|
-
end
|
|
60
|
-
else
|
|
61
|
-
conn
|
|
19
|
+
# Opens a session for this Generic Netlink family.
|
|
20
|
+
#
|
|
21
|
+
# @overload open(resolver:, executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
|
|
22
|
+
# The caller is responsible for closing the session.
|
|
23
|
+
# @param [#call] resolver a callable that resolves a family name to {FamilyInfo}
|
|
24
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
25
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
26
|
+
# @return [Family] the opened family session
|
|
27
|
+
# @overload open(resolver:, executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY, &block)
|
|
28
|
+
# The session is automatically closed after the block returns.
|
|
29
|
+
# @param [#call] resolver a callable that resolves a family name to {FamilyInfo}
|
|
30
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
31
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
32
|
+
# @yieldparam [Family] session the opened family session
|
|
33
|
+
# @return [Object] the value returned from the block
|
|
34
|
+
# @rbs (resolver: ^(Client, ::String) -> FamilyInfo, ?executor: executor?, ?notification_capacity: Integer?) -> (Nl::Family::Session & instance)
|
|
35
|
+
# | [R] (resolver: ^(Client, ::String) -> FamilyInfo, ?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
36
|
+
def self.open(resolver:, executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
|
|
37
|
+
begin
|
|
38
|
+
owner = Client.new(resolver:, executor:, notification_capacity:)
|
|
39
|
+
session = owner.family(self).extend(Nl::Family::Session)
|
|
40
|
+
rescue Exception
|
|
41
|
+
owner&.close
|
|
42
|
+
raise
|
|
62
43
|
end
|
|
63
|
-
|
|
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
|
|
44
|
+
return session unless block_given?
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
rescue NotImplementedError
|
|
77
|
-
@resolver.call(@socket, proto.name)
|
|
46
|
+
begin
|
|
47
|
+
yield session
|
|
48
|
+
ensure
|
|
49
|
+
session.close
|
|
78
50
|
end
|
|
79
|
-
family_class.new(@socket, protocol: Protocols::Genl.new(proto.name, family_id: id))
|
|
80
51
|
end
|
|
52
|
+
end
|
|
81
53
|
|
|
82
|
-
|
|
83
|
-
|
|
54
|
+
# Base class for Generic Netlink messages.
|
|
55
|
+
#
|
|
56
|
+
# A Generic Netlink message payload begins with a {GenlMsgHdr}, followed by
|
|
57
|
+
# the optional fixed header and attributes handled by {Raw::Message}.
|
|
58
|
+
class Message < Raw::Message
|
|
59
|
+
# Decodes a Generic Netlink message payload.
|
|
60
|
+
#
|
|
61
|
+
# @param [Decoder] decoder the source decoder
|
|
62
|
+
# @param [Integer] type the family ID from the Netlink message header
|
|
63
|
+
# @return [Message] the decoded message
|
|
64
|
+
# @raise [RuntimeError] if the command does not match the message class's type
|
|
65
|
+
# @rbs (Decoder decoder, type: Integer) -> instance
|
|
66
|
+
def self.decode(decoder, type:)
|
|
67
|
+
genlhdr = GenlMsgHdr.decode(decoder)
|
|
68
|
+
super(decoder, type: genlhdr.cmd)
|
|
84
69
|
end
|
|
85
70
|
end
|
|
86
71
|
end
|
|
87
72
|
end
|
|
73
|
+
|
|
74
|
+
require_relative 'genl/client'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require_relative 'error'
|
|
2
|
+
|
|
3
|
+
module Nl
|
|
4
|
+
# A multicast group. `name` is the kernel-facing name.
|
|
5
|
+
# `id` is the fixed ID when the specification provides one.
|
|
6
|
+
McastGroup = Data.define(:name, :id)
|
|
7
|
+
|
|
8
|
+
# Thread-safe queue shared by a family's blocking and asynchronous facades.
|
|
9
|
+
class NotificationChannel
|
|
10
|
+
def initialize(capacity:)
|
|
11
|
+
raise ArgumentError, 'notification capacity must be positive' if capacity && capacity <= 0
|
|
12
|
+
|
|
13
|
+
@capacity = capacity
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@condition = ConditionVariable.new
|
|
16
|
+
@queue = []
|
|
17
|
+
@error = nil
|
|
18
|
+
@closed = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Adds a notification without ever blocking the socket receive loop.
|
|
22
|
+
# Returns false after the channel has been closed.
|
|
23
|
+
def push(notification)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
return false if @closed
|
|
26
|
+
|
|
27
|
+
if @capacity && @queue.length >= @capacity
|
|
28
|
+
lose!(NotificationLossError.new('notification queue capacity exceeded'))
|
|
29
|
+
else
|
|
30
|
+
wake = @queue.empty? && !@error
|
|
31
|
+
@queue << notification
|
|
32
|
+
@condition.signal if wake
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Records a broken notification boundary while allowing later delivery to
|
|
39
|
+
# resume after the consumer observes the error and resynchronizes state.
|
|
40
|
+
def fail(error)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
return false if @closed
|
|
43
|
+
|
|
44
|
+
lose!(error)
|
|
45
|
+
end
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pop(timeout: nil)
|
|
50
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
51
|
+
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
loop do
|
|
54
|
+
if error = @error
|
|
55
|
+
@error = nil
|
|
56
|
+
raise error
|
|
57
|
+
end
|
|
58
|
+
return @queue.shift unless @queue.empty?
|
|
59
|
+
raise ClosedError, 'notification channel is closed' if @closed
|
|
60
|
+
|
|
61
|
+
remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
62
|
+
raise TimeoutError, 'notification receive timed out' if remaining && remaining <= 0
|
|
63
|
+
|
|
64
|
+
@condition.wait(@mutex, remaining)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def empty?
|
|
70
|
+
@mutex.synchronize { @queue.empty? && !@error }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def close
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
return nil if @closed
|
|
76
|
+
|
|
77
|
+
@closed = true
|
|
78
|
+
@queue.clear
|
|
79
|
+
@error = nil
|
|
80
|
+
@condition.broadcast
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def lose!(error)
|
|
86
|
+
@queue.clear
|
|
87
|
+
@error = error
|
|
88
|
+
@condition.signal
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# An unbounded-in-time, single-family view of unsolicited messages.
|
|
93
|
+
class NotificationStream
|
|
94
|
+
include Enumerable #[untyped]
|
|
95
|
+
|
|
96
|
+
def initialize(&receive)
|
|
97
|
+
@receive = receive
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def next(timeout: nil)
|
|
101
|
+
@receive.call(timeout)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @rbs () -> Enumerator[untyped, void]
|
|
105
|
+
# | () { (untyped) -> void } -> void
|
|
106
|
+
def each
|
|
107
|
+
return enum_for(__method__) unless block_given?
|
|
108
|
+
|
|
109
|
+
loop { yield self.next }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|