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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Rakefile +7 -0
  4. data/lib/nl/async/dispatcher.rb +199 -0
  5. data/lib/nl/async/driver.rb +37 -0
  6. data/lib/nl/async/mailbox.rb +81 -0
  7. data/lib/nl/async/operation.rb +277 -0
  8. data/lib/nl/async.rb +13 -0
  9. data/lib/nl/attribute_set.rb +173 -0
  10. data/lib/nl/bitfield32.rb +86 -0
  11. data/lib/nl/blocking_transport.rb +131 -0
  12. data/lib/nl/connection.rb +61 -0
  13. data/lib/nl/datagram.rb +22 -0
  14. data/lib/nl/datatypes.rb +454 -0
  15. data/lib/nl/decoder.rb +13 -1
  16. data/lib/nl/endian.rb +10 -0
  17. data/lib/nl/error.rb +10 -0
  18. data/lib/nl/exchange.rb +124 -0
  19. data/lib/nl/family.rb +117 -23
  20. data/lib/nl/genl/client.rb +104 -0
  21. data/lib/nl/genl/protocol.rb +59 -0
  22. data/lib/nl/genl/wire.rb +94 -0
  23. data/lib/nl/genl.rb +60 -73
  24. data/lib/nl/notification.rb +112 -0
  25. data/lib/nl/notification_router.rb +92 -0
  26. data/lib/nl/raw/client.rb +83 -0
  27. data/lib/nl/raw/protocol.rb +112 -0
  28. data/lib/nl/raw/wire.rb +146 -0
  29. data/lib/nl/raw.rb +104 -0
  30. data/lib/nl/sequence_allocator.rb +34 -0
  31. data/lib/nl/socket.rb +15 -12
  32. data/lib/nl/structured_payload.rb +61 -0
  33. data/lib/nl/sub_message.rb +94 -0
  34. data/lib/nl/version.rb +1 -1
  35. data/lib/nl.rb +10 -12
  36. data/sig/generated/nl/async/dispatcher.rbs +53 -0
  37. data/sig/generated/nl/async/driver.rbs +21 -0
  38. data/sig/generated/nl/async/mailbox.rbs +32 -0
  39. data/sig/generated/nl/async/operation.rbs +123 -0
  40. data/sig/generated/nl/async.rbs +9 -0
  41. data/sig/generated/nl/attribute_set.rbs +50 -0
  42. data/sig/generated/nl/bitfield32.rbs +41 -0
  43. data/sig/generated/nl/blocking_transport.rbs +33 -0
  44. data/sig/generated/nl/connection.rbs +26 -0
  45. data/sig/generated/nl/datagram.rbs +11 -0
  46. data/sig/generated/nl/datatypes.rbs +181 -0
  47. data/sig/generated/nl/decoder.rbs +38 -0
  48. data/sig/generated/nl/encoder.rbs +29 -0
  49. data/sig/generated/nl/endian.rbs +28 -0
  50. data/sig/generated/nl/error.rbs +27 -0
  51. data/sig/generated/nl/exchange.rbs +75 -0
  52. data/sig/generated/nl/family.rbs +71 -0
  53. data/sig/generated/nl/genl/client.rbs +74 -0
  54. data/sig/generated/nl/genl/protocol.rbs +37 -0
  55. data/sig/generated/nl/genl/wire.rbs +113 -0
  56. data/sig/generated/nl/genl.rbs +50 -0
  57. data/sig/generated/nl/notification.rbs +53 -0
  58. data/sig/generated/nl/notification_router.rbs +30 -0
  59. data/sig/generated/nl/raw/client.rbs +50 -0
  60. data/sig/generated/nl/raw/protocol.rbs +122 -0
  61. data/sig/generated/nl/raw/wire.rbs +151 -0
  62. data/sig/generated/nl/raw.rbs +70 -0
  63. data/sig/generated/nl/sequence_allocator.rbs +15 -0
  64. data/sig/generated/nl/socket.rbs +40 -0
  65. data/sig/generated/nl/structured_payload.rbs +25 -0
  66. data/sig/generated/nl/sub_message.rbs +74 -0
  67. data/sig/generated/nl/version.rbs +5 -0
  68. data/sig/generated/nl.rbs +2 -0
  69. metadata +65 -9
  70. data/lib/nl/core.rb +0 -94
  71. data/lib/nl/protocols/genl.rb +0 -55
  72. data/lib/nl/protocols/raw.rb +0 -395
data/lib/nl/family.rb CHANGED
@@ -1,37 +1,131 @@
1
- #--
2
- # rbs_inline: enabled
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 socket: Socket
9
- # @rbs protocol: Protocol
46
+ # @rbs connection: _Connection
47
+ # @rbs endpoint: Raw::Endpoint
10
48
  # @rbs return: instance
11
- def initialize(socket, protocol: self.class::PROTOCOL)
12
- @socket = socket
13
- @protocol = protocol
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 () -> instance
18
- # | [R] () { (instance) -> R } -> R
19
- def self.open
20
- begin
21
- socket = Socket.new(self::PROTOCOL.protonum)
22
- socket.bind(Socket.sockaddr_nl(0, 0))
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 exchange_message(type, request_class, reply_class, args, &block)
34
- @protocol.exchange_message(@socket, type, request_class, reply_class, args, &block)
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
@@ -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
- # General Netlink message handling
1
+ # Generic Netlink family support
2
2
 
3
- require_relative 'core'
4
- require_relative 'endian'
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
- module Constants
9
- # From include/uapi/linux/genetlink.
10
- GENL_NAMSIZ = 16
11
- GENL_MIN_ID = Core::NLMSG_MIN_TYPE
12
- GENL_MAX_ID = 1023
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
- GENL_HDRLEN = 4
15
-
16
- GENL_ID_GENERATE = 0
17
- GENL_ID_CTRL = Core::NLMSG_MIN_TYPE
18
- GENL_ID_VFS_DQUOT = Core::NLMSG_MIN_TYPE + 1
19
- GENL_ID_PMCRAID = Core::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
- 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
- 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
44
+ return session unless block_given?
71
45
 
72
- def open(family_class)
73
- proto = family_class::PROTOCOL
74
- id = @id_cache[proto.name] ||= begin
75
- proto.family_id
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
- def close
83
- @socket.close
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