systemd-journal 0.1.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eb534e3c9f9996d4a6364655a349306492464e69
4
- data.tar.gz: 27bea2ecb30d9a9fef2767050ba1df2c2bd01e46
3
+ metadata.gz: 871a098e73eadfe6cd53fc8066537c37e1a1681b
4
+ data.tar.gz: cb944399a1624c724b469ca41b13a3ccb4bddcb0
5
5
  SHA512:
6
- metadata.gz: b4443ed8d9c9820483dbab619a5616d896bb5d4de1722894f0d840800a29ac32c07036f396ba9eb8fa5ade33ff633203a55a483fb238a68f6a67351ce3b532dd
7
- data.tar.gz: 5d491da1ce955d6ac9ac2ffbdda95af9e633404d0db9282705cb84138c8cbf66099f27dee17ab63964231cb1609b9667f986b64a6b8b4e3af2098a18f56550cf
6
+ metadata.gz: a399b0ddb5e9eb60bb305b631bb531588352f0015a1ac0b64880fa289877a716c44eaa34d887461a17144af21f5c520a6d759c8030c315538c117160ead48724
7
+ data.tar.gz: af798df9d31d0cc9e742104de505964bf1e07f94dfb74918407cdce0996cde716a96b440482614c57050afabc56c62aa82d218edc53fb20f3685e29ec5c1d719
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # Systemd::Journal
1
+ # Systemd::Journal [![Gem Version](https://badge.fury.io/rb/systemd-journal.png)](http://badge.fury.io/rb/systemd-journal) [![Build Status](https://travis-ci.org/ledbettj/systemd-journal.png?branch=master)](https://travis-ci.org/ledbettj/systemd-journal)
2
2
 
3
3
  Ruby bindings for reading from the systemd journal.
4
4
 
5
- * [documentation](http://rubydoc.info/github/ledbettj/systemd-journal/Systemd/Journal)
5
+ * [documentation](http://rubydoc.info/gems/systemd-journal)
6
6
 
7
7
  ## Installation
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
- gem 'systemd-journal', '~> 0.1.0'
11
+ gem 'systemd-journal', '~> 1.0.0'
12
12
 
13
13
  And then execute:
14
14
 
@@ -16,29 +16,33 @@ And then execute:
16
16
 
17
17
  ## Usage
18
18
 
19
- For example, printing all messages:
19
+ Print all messages as they occur:
20
20
 
21
21
  require 'systemd/journal'
22
22
 
23
23
  j = Systemd::Journal.new
24
-
25
- while j.move_next
26
- puts j.read_field('MESSAGE')
24
+ j.seek(:tail)
25
+
26
+ j.watch do |entry|
27
+ puts entry.message
27
28
  end
28
-
29
- Or to print all data in each entry:
29
+
30
+ Filter messages included in the journal:
30
31
 
31
32
  require 'systemd/journal'
32
-
33
+
33
34
  j = Systemd::Journal.new
34
-
35
+
36
+ # only display entries from SSHD with priority 6.
37
+ j.add_match(:priority, 6)
38
+ j.add_match(:_exe, '/usr/bin/sshd')
39
+
35
40
  while j.move_next
36
- j.current_entry do |key, value|
37
- puts "#{key}: #{value}"
38
- end
39
- puts "\n"
41
+ puts j.current_entry.message
40
42
  end
41
43
 
44
+ See the documentation for more examples.
45
+
42
46
  ## Contributing
43
47
 
44
48
  1. Fork it
data/Rakefile CHANGED
@@ -13,5 +13,9 @@ YARD::Rake::YardocTask.new do |t|
13
13
  end
14
14
 
15
15
  RSpec::Core::RakeTask.new(:spec) do |t|
16
- t.rspec_opts = "--color"
16
+ opts = ['--color']
17
+ opts << '--require ./spec/no_ffi.rb' if ENV['TRAVIS']
18
+ t.rspec_opts = opts.join(' ')
17
19
  end
20
+
21
+ task default: :spec
@@ -8,9 +8,7 @@ if ARGV.length == 0
8
8
  end
9
9
 
10
10
  j = Systemd::Journal.new(path: ARGV[0])
11
- j.seek(:head)
12
11
 
13
- while j.move_next
14
- entry = j.current_entry
15
- puts "PID #{entry['_PID']}: #{entry['MESSAGE']}"
12
+ j.each do |entry|
13
+ puts entry
16
14
  end
@@ -8,16 +8,11 @@ class SSHWatcher
8
8
  end
9
9
 
10
10
  def run
11
- @journal.add_match('_EXE', '/usr/bin/sshd')
11
+ @journal.add_match(:_exe, '/usr/bin/sshd')
12
12
  # skip all existing entries -- sd_journal_seek_tail() is currently broken.
13
13
  while @journal.move_next ; end
14
14
 
15
- while true
16
- if @journal.wait(1_000_000 * 5) != :nop
17
- process_event(@journal.current_entry) while @journal.move_next
18
- end
19
- end
20
-
15
+ @journal.watch{ |entry| process_event(entry) }
21
16
  end
22
17
 
23
18
  private
@@ -25,9 +20,9 @@ class SSHWatcher
25
20
  LOGIN_REGEXP = /Accepted\s+(?<auth_method>[^\s]+)\s+for\s+(?<user>[^\s]+)\s+from\s+(?<address>[^\s]+)/
26
21
 
27
22
  def process_event(entry)
28
- if (m = entry['MESSAGE'].match(LOGIN_REGEXP))
23
+ if (m = entry.message.match(LOGIN_REGEXP))
29
24
  timestamp = DateTime.strptime(
30
- (entry['_SOURCE_REALTIME_TIMESTAMP'].to_i / 1_000_000).to_s,
25
+ (entry._source_realtime_timestamp.to_i / 1_000_000).to_s,
31
26
  "%s"
32
27
  )
33
28
  puts "login via #{m[:auth_method]} for #{m[:user]} from #{m[:address]} at #{timestamp.ctime}"
@@ -0,0 +1,19 @@
1
+ require 'ffi'
2
+
3
+ # @private
4
+ class FFI::MemoryPointer
5
+
6
+ # monkey patch a read_size_t and write_size_t method onto
7
+ # FFI::MemoryPointer if necessary.
8
+ case (p = FFI::MemoryPointer.new(:size_t, 1)).size
9
+ when 4
10
+ alias_method(:read_size_t, :read_uint32) unless p.respond_to?(:read_size_t)
11
+ alias_method(:write_size_t, :write_uint32) unless p.respond_to?(:write_size_t)
12
+ when 8
13
+ alias_method(:read_size_t, :read_uint64) unless p.respond_to?(:read_size_t)
14
+ alias_method(:write_size_t, :write_uint64) unless p.respond_to?(:write_size_t)
15
+ else
16
+ raise RuntimeError.new("unsupported size_t width: #{p.size}")
17
+ end
18
+
19
+ end
data/lib/systemd/id128.rb CHANGED
@@ -46,9 +46,15 @@ module Systemd
46
46
 
47
47
  # providing bindings to the systemd-id128 library.
48
48
  module Native
49
- require 'ffi'
50
- extend FFI::Library
51
- ffi_lib %w[libsystemd-id128.so libsystemd-id128.so.0]
49
+ unless $NO_FFI_SPEC
50
+ require 'ffi'
51
+ extend FFI::Library
52
+ ffi_lib %w[libsystemd-id128.so libsystemd-id128.so.0]
53
+
54
+ attach_function :sd_id128_get_machine, [:pointer], :int
55
+ attach_function :sd_id128_get_boot, [:pointer], :int
56
+ attach_function :sd_id128_randomize, [:pointer], :int
57
+ end
52
58
 
53
59
  class Id128 < FFI::Union
54
60
  layout :bytes, [:uint8, 16],
@@ -59,9 +65,6 @@ module Systemd
59
65
  ("%02x" * 16) % self[:bytes].to_a
60
66
  end
61
67
  end
62
- attach_function :sd_id128_get_machine, [:pointer], :int
63
- attach_function :sd_id128_get_boot, [:pointer], :int
64
- attach_function :sd_id128_randomize, [:pointer], :int
65
68
  end
66
69
  end
67
70
  end
@@ -33,6 +33,14 @@ module Systemd
33
33
  # {Systemd::Journal}
34
34
  module ClassMethods
35
35
 
36
+ # write the value of the c errno constant to the systemd journal in the
37
+ # style of the perror() function.
38
+ # @param [String] message the text to prefix the error message with.
39
+ def perror(message)
40
+ rc = Native::sd_journal_perror(message)
41
+ raise JournalError.new(rc) if rc < 0
42
+ end
43
+
36
44
  # write a simple message to the systemd journal.
37
45
  # @param [Integer] level one of the LOG_* constants defining the
38
46
  # severity of the event.
@@ -1,8 +1,9 @@
1
1
  module Systemd
2
2
  class Journal
3
3
  # contains a set of constants which maybe bitwise OR-ed together and passed
4
- # to the Journal constructor:
5
- # `Journal.new(flags: Systemd::Journal::Flags::LOCAL_ONLY)`
4
+ # to the Journal constructor.
5
+ # @example
6
+ # Systemd::Journal.new(flags: Systemd::Journal::Flags::LOCAL_ONLY)
6
7
  module Flags
7
8
  # Only open journal files generated on the local machine.
8
9
  LOCAL_ONLY = 1
@@ -22,11 +22,18 @@ module Systemd
22
22
  attach_function :sd_journal_seek_tail, [:pointer], :int
23
23
  attach_function :sd_journal_seek_realtime_usec, [:pointer, :uint64], :int
24
24
 
25
+ attach_function :sd_journal_get_cursor, [:pointer, :pointer], :int
26
+ attach_function :sd_journal_seek_cursor, [:pointer, :string], :int
27
+ attach_function :sd_journal_test_cursor, [:pointer, :string], :int
28
+
25
29
  # data reading
26
30
  attach_function :sd_journal_get_data, [:pointer, :string, :pointer, :pointer], :int
27
31
  attach_function :sd_journal_restart_data, [:pointer], :void
28
32
  attach_function :sd_journal_enumerate_data, [:pointer, :pointer, :pointer], :int
29
33
 
34
+ attach_function :sd_journal_get_data_threshold, [:pointer, :pointer], :int
35
+ attach_function :sd_journal_set_data_threshold, [:pointer, :size_t], :int
36
+
30
37
  # querying
31
38
  attach_function :sd_journal_query_unique, [:pointer, :string], :int
32
39
  attach_function :sd_journal_enumerate_unique, [:pointer, :pointer, :pointer], :int
@@ -38,7 +45,7 @@ module Systemd
38
45
  :append,
39
46
  :invalidate
40
47
  ]
41
- attach_function :sd_journal_wait, [:pointer, :uint64], :wake_reason
48
+ attach_function :sd_journal_wait, [:pointer, :uint64], :wake_reason, blocking: true
42
49
 
43
50
  # filtering
44
51
  attach_function :sd_journal_add_match, [:pointer, :string, :size_t], :int
@@ -47,12 +54,12 @@ module Systemd
47
54
  attach_function :sd_journal_add_conjunction, [:pointer], :int
48
55
 
49
56
  # writing
50
- attach_function :sd_journal_print, [:int, :string], :int
51
- attach_function :sd_journal_send, [:varargs], :int
52
-
57
+ attach_function :sd_journal_print, [:int, :string], :int
58
+ attach_function :sd_journal_send, [:varargs], :int
59
+ attach_function :sd_journal_perror, [:string], :int
53
60
  # misc
54
61
  attach_function :sd_journal_get_usage, [:pointer, :pointer], :int
55
62
  end
56
63
 
57
- end
64
+ end unless $NO_FFI_SPEC
58
65
  end
@@ -1,6 +1,6 @@
1
1
  module Systemd
2
2
  class Journal
3
3
  # The version of the systemd-journal gem.
4
- VERSION = '0.1.4'
4
+ VERSION = '1.0.0'
5
5
  end
6
6
  end
@@ -3,8 +3,11 @@ require 'systemd/journal/flags'
3
3
  require 'systemd/journal/compat'
4
4
  require 'systemd/journal/fields'
5
5
  require 'systemd/journal_error'
6
+ require 'systemd/journal_entry'
6
7
  require 'systemd/id128'
7
8
 
9
+ require 'systemd/ffi_size_t'
10
+
8
11
  module Systemd
9
12
  # Class to allow interacting with the systemd journal.
10
13
  # To read from the journal, instantiate a new {Systemd::Journal}; to write to
@@ -12,6 +15,7 @@ module Systemd
12
15
  # {Systemd::Journal::Compat::ClassMethods#message Journal.message} or
13
16
  # {Systemd::Journal::Compat::ClassMethods#print Journal.print}.
14
17
  class Journal
18
+ include Enumerable
15
19
  include Systemd::Journal::Compat
16
20
 
17
21
  # Returns a new instance of a Journal, opened with the provided options.
@@ -45,17 +49,62 @@ module Systemd
45
49
  ObjectSpace.define_finalizer(self, self.class.finalize(@ptr))
46
50
  end
47
51
 
52
+ # Iterate over each entry in the journal, respecting the applied
53
+ # conjunctions/disjunctions.
54
+ # If a block is given, it is called with each entry until no more
55
+ # entries remain. Otherwise, returns an enumerator which can be chained.
56
+ def each
57
+ return to_enum(:each) unless block_given?
58
+
59
+ seek(:head)
60
+ yield current_entry while move_next
61
+ end
62
+
63
+ # Move the read pointer by `offset` entries.
64
+ # @param [Integer] offset how many entries to move the read pointer by. If
65
+ # this value is positive, the read pointer moves forward. Otherwise, it
66
+ # moves backwards.
67
+ # @return [Integer] the number of entries the read pointer actually moved.
68
+ def move(offset)
69
+ offset > 0 ? move_next_skip(offset) : move_previous_skip(-offset)
70
+ end
71
+
72
+ # Filter the journal at a high level.
73
+ # Takes any number of arguments; each argument should be a hash representing
74
+ # a condition to filter based on. Fields inside the hash will be ANDed
75
+ # together. Each hash will be ORed with the others. Fields in hashes with
76
+ # Arrays as values are treated as an OR statement, since otherwise they
77
+ # would never match.
78
+ # @example
79
+ # j = Systemd::Journal.filter(
80
+ # {_systemd_unit: 'session-4.scope'},
81
+ # {priority: [4, 6]},
82
+ # {_exe: '/usr/bin/sshd', priority: 1}
83
+ # )
84
+ # # equivalent to
85
+ # (_systemd_unit == 'session-4.scope') ||
86
+ # (priority == 4 || priority == 6) ||
87
+ # (_exe == '/usr/bin/sshd' && priority == 1)
88
+ def filter(*conditions)
89
+ clear_filters
90
+
91
+ last_index = conditions.length - 1
92
+
93
+ conditions.each_with_index do |condition, index|
94
+ add_filters(condition)
95
+ add_disjunction unless index == last_index
96
+ end
97
+ end
98
+
48
99
  # Move the read pointer to the next entry in the journal.
49
100
  # @return [Boolean] True if moving to the next entry was successful.
50
101
  # @return [Boolean] False if unable to move to the next entry, indicating
51
102
  # that the pointer has reached the end of the journal.
52
103
  def move_next
53
- case (rc = Native::sd_journal_next(@ptr))
54
- when 0 then false
55
- when 1 then true
56
- else
104
+ if (rc = Native::sd_journal_next(@ptr)) < 0
57
105
  raise JournalError.new(rc) if rc < 0
58
106
  end
107
+ rc > 0
59
108
  end
60
109
 
61
110
  # Move the read pointer forward by `amount` entries.
@@ -73,12 +122,10 @@ module Systemd
73
122
  # @return [Boolean] False if unable to move to the previous entry,
74
123
  # indicating that the pointer has reached the beginning of the journal.
75
124
  def move_previous
76
- case (rc = Native::sd_journal_previous(@ptr))
77
- when 0 then false # EOF
78
- when 1 then true
79
- else
125
+ if (rc = Native::sd_journal_previous(@ptr)) < 0
80
126
  raise JournalError.new(rc) if rc < 0
81
127
  end
128
+ rc > 0
82
129
  end
83
130
 
84
131
  # Move the read pointer backwards by `amount` entries.
@@ -98,7 +145,9 @@ module Systemd
98
145
  # @param [Symbol, Time] whence one of :head, :tail, or a Time instance.
99
146
  # `:head` (or `:start`) will seek to the beginning of the journal.
100
147
  # `:tail` (or `:end`) will seek to the end of the journal. When a `Time`
101
- # is provided, seek to the journal entry logged closest to that time.
148
+ # is provided, seek to the journal entry logged closest to that time. When
149
+ # a String is provided, assume it is a cursor from {#cursor} and seek to
150
+ # that entry.
102
151
  # @return [True]
103
152
  def seek(whence)
104
153
  rc = case whence
@@ -110,6 +159,8 @@ module Systemd
110
159
  if whence.is_a?(Time)
111
160
  # TODO: is this right? who knows.
112
161
  Native::sd_journal_seek_realtime_usec(@ptr, whence.to_i * 1_000_000)
162
+ elsif whence.is_a?(String)
163
+ Native::sd_journal_seek_cursor(@ptr, whence)
113
164
  else
114
165
  raise ArgumentError.new("Unknown seek type: #{whence.class}")
115
166
  end
@@ -133,11 +184,11 @@ module Systemd
133
184
  len_ptr = FFI::MemoryPointer.new(:size_t, 1)
134
185
  out_ptr = FFI::MemoryPointer.new(:pointer, 1)
135
186
 
136
- rc = Native::sd_journal_get_data(@ptr, field, out_ptr, len_ptr)
187
+ rc = Native::sd_journal_get_data(@ptr, field.to_s.upcase, out_ptr, len_ptr)
137
188
 
138
189
  raise JournalError.new(rc) if rc < 0
139
190
 
140
- len = read_size_t(len_ptr)
191
+ len = len_ptr.read_size_t
141
192
  out_ptr.read_pointer.read_string_length(len).split('=', 2).last
142
193
  end
143
194
 
@@ -161,7 +212,7 @@ module Systemd
161
212
  results = {}
162
213
 
163
214
  while (rc = Native::sd_journal_enumerate_data(@ptr, out_ptr, len_ptr)) > 0
164
- len = read_size_t(len_ptr)
215
+ len = len_ptr.read_size_t
165
216
  key, value = out_ptr.read_pointer.read_string_length(len).split('=', 2)
166
217
  results[key] = value
167
218
 
@@ -170,7 +221,7 @@ module Systemd
170
221
 
171
222
  raise JournalError.new(rc) if rc < 0
172
223
 
173
- results
224
+ JournalEntry.new(results)
174
225
  end
175
226
 
176
227
  # Get the list of unique values stored in the journal for the given field.
@@ -186,18 +237,17 @@ module Systemd
186
237
  # end
187
238
  def query_unique(field)
188
239
  results = []
189
- field = field.to_s.upcase
190
240
  out_ptr = FFI::MemoryPointer.new(:pointer, 1)
191
241
  len_ptr = FFI::MemoryPointer.new(:size_t, 1)
192
242
 
193
243
  Native::sd_journal_restart_unique(@ptr)
194
244
 
195
- if (rc = Native::sd_journal_query_unique(@ptr, field)) < 0
245
+ if (rc = Native::sd_journal_query_unique(@ptr, field.to_s.upcase)) < 0
196
246
  raise JournalError.new(rc)
197
247
  end
198
248
 
199
249
  while (rc = Native::sd_journal_enumerate_unique(@ptr, out_ptr, len_ptr)) > 0
200
- len = read_size_t(len_ptr)
250
+ len = len_ptr.read_size_t
201
251
  results << out_ptr.read_pointer.read_string_length(len).split('=', 2).last
202
252
 
203
253
  yield results.last if block_given?
@@ -214,41 +264,73 @@ module Systemd
214
264
  # @example Wait for an event for a maximum of 3 seconds
215
265
  # j = Systemd::Journal.new
216
266
  # j.seek(:tail)
217
- # if j.wait(3 * 1_000_000) != :nop
267
+ # if j.wait(3 * 1_000_000)
218
268
  # # event occurred
219
269
  # end
220
- # @return [Symbol] :nop if the wait time was reached (no events occured).
270
+ # @return [Nil] if the wait time was reached (no events occured).
221
271
  # @return [Symbol] :append if new entries were appened to the journal.
222
272
  # @return [Symbol] :invalidate if journal files were added/removed/rotated.
223
273
  def wait(timeout_usec = -1)
224
274
  rc = Native::sd_journal_wait(@ptr, timeout_usec)
225
275
  raise JournalError.new(rc) if rc.is_a?(Fixnum) && rc < 0
226
- rc
276
+ rc == :nop ? nil : rc
277
+ end
278
+
279
+ # Blocks and waits for new entries to be appended to the journal. When new
280
+ # entries are written, yields them in turn. Note that this function does
281
+ # not automatically seek to the end of the journal prior to waiting.
282
+ # This method Does not return.
283
+ # @example Print out events as they happen
284
+ # j = Systemd::Journal.new
285
+ # j.seek(:tail)
286
+ # j.watch do |event|
287
+ # puts event.message
288
+ # end
289
+ def watch
290
+ while true
291
+ if wait
292
+ yield current_entry while move_next
293
+ end
294
+ end
227
295
  end
228
296
 
229
297
  # Add a filter to journal, such that only entries where the given filter
230
298
  # matches are returned.
231
- # {#move_next} or {#move_previous} must be invoked after adding a match
299
+ # {#move_next} or {#move_previous} must be invoked after adding a filter
232
300
  # before attempting to read from the journal.
233
301
  # @param [String] field the column to filter on, e.g. _PID, _EXE.
234
302
  # @param [String] value the match to search for, e.g. '/usr/bin/sshd'
235
303
  # @return [nil]
236
- def add_match(field, value)
304
+ def add_filter(field, value)
237
305
  match = "#{field.to_s.upcase}=#{value}"
238
306
  rc = Native::sd_journal_add_match(@ptr, match, match.length)
239
307
  raise JournalError.new(rc) if rc < 0
240
308
  end
241
309
 
310
+ # Add a set of filters to the journal, such that only entries where the
311
+ # given filters match are returned.
312
+ # @param [Hash] filters a set of field/filter value pairs.
313
+ # If the filter value is an array, each value in the array is added
314
+ # and entries where the specified field matches any of the values is
315
+ # returned.
316
+ # @example Filter by PID and EXE
317
+ # j.add_filters(_pid: 6700, _exe: '/usr/bin/sshd')
318
+ def add_filters(filters)
319
+ filters.each do |field, value|
320
+ Array(value).each{ |v| add_filter(field, v) }
321
+ end
322
+ end
323
+
242
324
  # Add an OR condition to the filter. All previously added matches
243
- # and any matches added afterwards will be OR-ed together.
325
+ # will be ORed with the terms following the disjunction.
244
326
  # {#move_next} or {#move_previous} must be invoked after adding a match
245
327
  # before attempting to read from the journal.
246
328
  # @return [nil]
247
329
  # @example Filter entries returned using an OR condition
248
330
  # j = Systemd::Journal.new
249
- # j.add_match('PRIORITY', 5)
250
- # j.add_match('_EXE', '/usr/bin/sshd')
331
+ # j.add_filter('PRIORITY', 5)
251
332
  # j.add_disjunction
333
+ # j.add_filter('_EXE', '/usr/bin/sshd')
252
334
  # while j.move_next
253
335
  # # current_entry is either an sshd event or
254
336
  # # has priority 5
@@ -258,16 +340,16 @@ module Systemd
258
340
  raise JournalError.new(rc) if rc < 0
259
341
  end
260
342
 
261
- # Add an AND condition to the filter. All previously added matches
262
- # and any matches added afterwards will be AND-ed together.
343
+ # Add an AND condition to the filter. All previously added terms will be
344
+ # ANDed together with terms following the conjunction.
263
345
  # {#move_next} or {#move_previous} must be invoked after adding a match
264
346
  # before attempting to read from the journal.
265
347
  # @return [nil]
266
348
  # @example Filter entries returned using an AND condition
267
349
  # j = Systemd::Journal.new
268
- # j.add_match('PRIORITY', 5)
269
- # j.add_match('_EXE', '/usr/bin/sshd')
350
+ # j.add_filter('PRIORITY', 5)
270
351
  # j.add_conjunction
352
+ # j.add_filter('_EXE', '/usr/bin/sshd')
271
353
  # while j.move_next
272
354
  # # current_entry is an sshd event with priority 5
273
355
  # end
@@ -276,9 +358,9 @@ module Systemd
276
358
  raise JournalError.new(rc) if rc < 0
277
359
  end
278
360
 
279
- # Remove all matches and conjunctions/disjunctions.
361
+ # Remove all filters and conjunctions/disjunctions.
280
362
  # @return [nil]
281
- def clear_matches
363
+ def clear_filters
282
364
  Native::sd_journal_flush_matches(@ptr)
283
365
  end
284
366
 
@@ -295,21 +377,55 @@ module Systemd
295
377
  size_ptr.read_uint64
296
378
  end
297
379
 
298
- private
380
+ # Get the maximum length of a data field that will be returned.
381
+ # Fields longer than this will be truncated. Default is 64K.
382
+ # @return [Integer] size in bytes.
383
+ def data_threshold
384
+ size_ptr = FFI::MemoryPointer.new(:size_t, 1)
385
+ if (rc = Native::sd_journal_get_data_threshold(@ptr, size_ptr)) < 0
386
+ raise JournalError.new(rc)
387
+ end
299
388
 
300
- def self.finalize(ptr)
301
- proc{ Native::sd_journal_close(ptr) unless ptr.nil? }
389
+ size_ptr.read_size_t
390
+ end
391
+
392
+ # Set the maximum length of a data field that will be returned.
393
+ # Fields longer than this will be truncated.
394
+ def data_threshold=(threshold)
395
+ if (rc = Native::sd_journal_set_data_threshold(@ptr, threshold)) < 0
396
+ raise JournalError.new(rc)
397
+ end
398
+ end
399
+
400
+ # returns a string representing the current read position.
401
+ # This string can be passed to {#seek} or {#cursor?}.
402
+ # @return [String] a cursor token.
403
+ def cursor
404
+ out_ptr = FFI::MemoryPointer.new(:pointer, 1)
405
+ if (rc = Native.sd_journal_get_cursor(@ptr, out_ptr)) < 0
406
+ raise JournalError.new(rc)
407
+ end
408
+
409
+ out_ptr.read_pointer.read_string
302
410
  end
303
411
 
304
- def read_size_t(ptr)
305
- case ptr.size
306
- when 8
307
- ptr.read_uint64
308
- when 4
309
- ptr.read_uint32
310
- else
311
- raise StandardError.new("Unhandled size_t size: #{ptr.size}")
412
+ # Check if the read position is currently at the entry represented by the
413
+ # provided cursor value.
414
+ # @param c [String] a cursor token returned from {#cursor}.
415
+ # @return [Boolean] True if the current entry is the one represented by the
416
+ # provided cursor, False otherwise.
417
+ def cursor?(c)
418
+ if (rc = Native.sd_journal_test_cursor(@ptr, c)) < 0
419
+ raise JournalError.new(rc)
312
420
  end
421
+
422
+ rc > 0
423
+ end
424
+
425
+ private
426
+
427
+ def self.finalize(ptr)
428
+ proc{ Native::sd_journal_close(ptr) unless ptr.nil? }
313
429
  end
314
430
 
315
431
  end
@@ -0,0 +1,28 @@
1
+ module Systemd
2
+ class JournalEntry
3
+ include Enumerable
4
+
5
+ attr_reader :fields
6
+
7
+ def initialize(entry)
8
+ @entry = entry
9
+ @fields = entry.map do |key, value|
10
+ name = key.downcase.to_sym
11
+ define_singleton_method(name){ value } unless respond_to?(name)
12
+ name
13
+ end
14
+
15
+ end
16
+
17
+ def [](key)
18
+ @entry[key] || @entry[key.to_s.upcase]
19
+ end
20
+
21
+ def each
22
+ return to_enum(:each) unless block_given?
23
+
24
+ @entry.each{ |key, value| yield [key, value] }
25
+ end
26
+
27
+ end
28
+ end
@@ -1,3 +1,5 @@
1
+ require 'ffi'
2
+
1
3
  module Systemd
2
4
  # This execption is raised whenever a sd_journal_* call returns an error.
3
5
  class JournalError < StandardError
data/spec/no_ffi.rb ADDED
@@ -0,0 +1,4 @@
1
+ $NO_FFI_SPEC = true
2
+ module Systemd::Journal::Native ; end
3
+
4
+ puts "Warning: running specs without libsystemd-journal and libsystemd-id128"
data/spec/spec_helper.rb CHANGED
@@ -3,3 +3,37 @@ require 'simplecov'
3
3
 
4
4
  SimpleCov.start
5
5
  require 'systemd/journal'
6
+
7
+ RSpec.configure do |config|
8
+ config.before(:each) do
9
+
10
+ # Stub open and close calls
11
+ dummy_open = ->(ptr, flags, path=nil) do
12
+ ptr.write_pointer(nil)
13
+ 0
14
+ end
15
+
16
+ Systemd::Journal::Native.stub(:sd_journal_open, &dummy_open)
17
+ Systemd::Journal::Native.stub(:sd_journal_open_directory, &dummy_open)
18
+ Systemd::Journal::Native.stub(:sd_journal_close).and_return(0)
19
+
20
+ # Raise an exception if any native calls are actually called
21
+ native_calls = Systemd::Journal::Native.methods.select do |m|
22
+ m.to_s.start_with?("sd_")
23
+ end
24
+
25
+ native_calls -= [
26
+ :sd_journal_open, :sd_journal_open_directory, :sd_journal_close
27
+ ]
28
+
29
+ build_err_proc = ->(method_name) do
30
+ return ->(*params) do
31
+ raise RuntimeError.new("#{method_name} called without being stubbed.")
32
+ end
33
+ end
34
+
35
+ native_calls.each do |meth|
36
+ Systemd::Journal::Native.stub(meth, &build_err_proc.call(meth))
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe Systemd::JournalEntry do
4
+ subject do
5
+ Systemd::JournalEntry.new(
6
+ '_PID' => '125',
7
+ '_EXE' => '/usr/bin/sshd',
8
+ 'PRIORITY' => '4',
9
+ 'OBJECT_ID'=> ':)'
10
+ )
11
+ end
12
+
13
+ it 'allows enumerating entries' do
14
+ expect{ |b| subject.each(&b) }.to yield_successive_args(
15
+ ['_PID', '125'],
16
+ ['_EXE', '/usr/bin/sshd'],
17
+ ['PRIORITY', '4'],
18
+ ['OBJECT_ID', ':)']
19
+ )
20
+ end
21
+
22
+ it 'responds to field names as methods' do
23
+ subject._pid.should eq('125')
24
+ subject.priority.should eq('4')
25
+ end
26
+
27
+ it 'doesnt overwrite existing methods' do
28
+ subject.object_id.should_not eq(':)')
29
+ end
30
+
31
+ it 'allows accessing via [string]' do
32
+ subject['OBJECT_ID'].should eq(':)')
33
+ end
34
+
35
+ it 'allows accessing via [symbol]' do
36
+ subject[:object_id].should eq(':)')
37
+ end
38
+
39
+ it 'lists all fields it contains' do
40
+ subject.fields.should eq([:_pid, :_exe, :priority, :object_id])
41
+ end
42
+
43
+ end
@@ -1,18 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Systemd::Journal do
4
-
5
- before(:each) do
6
- # don't actually make native API calls.
7
- dummy_open = ->(ptr, flags, path=nil) do
8
- ptr.write_pointer(nil)
9
- 0
10
- end
11
-
12
- Systemd::Journal::Native.stub(:sd_journal_open, &dummy_open)
13
- Systemd::Journal::Native.stub(:sd_journal_open_directory, &dummy_open)
14
- Systemd::Journal::Native.stub(:sd_journal_close).and_return(0)
15
- end
16
4
 
17
5
  describe '#initialize' do
18
6
  it 'opens a directory if a path is passed' do
@@ -34,6 +22,20 @@ describe Systemd::Journal do
34
22
  end
35
23
  end
36
24
 
25
+ describe '#move' do
26
+ it 'calls move_next_skip if the value is positive' do
27
+ j = Systemd::Journal.new
28
+ j.should_receive(:move_next_skip).with(5)
29
+ j.move(5)
30
+ end
31
+
32
+ it 'calls move_next_previous otherwise' do
33
+ j = Systemd::Journal.new
34
+ j.should_receive(:move_previous_skip).with(5)
35
+ j.move(-5)
36
+ end
37
+ end
38
+
37
39
  ['next', 'previous'].each do |direction|
38
40
  describe "#move_#{direction}" do
39
41
  it 'returns true on a successful move' do
@@ -76,6 +78,33 @@ describe Systemd::Journal do
76
78
  end
77
79
  end
78
80
 
81
+ describe '#each' do
82
+ it 'should reposition to the head of the journal' do
83
+ j = Systemd::Journal.new
84
+ j.should_receive(:seek).with(:head).and_return(0)
85
+ j.stub(:move_next).and_return(nil)
86
+ j.each{|e| nil }
87
+ end
88
+
89
+ it 'should return an enumerator if no block is given' do
90
+ j = Systemd::Journal.new
91
+ j.each.class.should eq(Enumerator)
92
+ end
93
+
94
+ it 'should return each entry in the journal' do
95
+ entries = [{'_PID' => 1}, {'_PID' => 2}]
96
+ entry = nil
97
+
98
+ j = Systemd::Journal.new
99
+ j.stub(:seek).and_return(0)
100
+ j.stub(:current_entry) { entry }
101
+ j.stub(:move_next) { entry = entries.shift }
102
+
103
+ j.map{|e| e['_PID'] }.should eq([1, 2])
104
+ end
105
+
106
+ end
107
+
79
108
  describe '#seek' do
80
109
  it 'moves to the first entry of the file' do
81
110
  j = Systemd::Journal.new
@@ -94,44 +123,226 @@ describe Systemd::Journal do
94
123
  Systemd::Journal::Native.should_receive(:sd_journal_seek_realtime_usec).and_return(0)
95
124
  j.seek(Time.now).should eq(true)
96
125
  end
126
+
127
+ it 'seeks based on a cursor when a string is provided' do
128
+ j = Systemd::Journal.new
129
+
130
+ Systemd::Journal::Native.should_receive(:sd_journal_seek_cursor).
131
+ with(anything, "123").and_return(0)
132
+
133
+ j.seek("123")
134
+ end
135
+
136
+ it 'throws an exception if it doesnt understand the type' do
137
+ j = Systemd::Journal.new
138
+ expect { j.seek(Object.new) }.to raise_error(ArgumentError)
139
+ end
97
140
  end
98
141
 
99
142
  describe '#read_field' do
100
- pending
143
+ it 'raises an exception if the call fails' do
144
+ Systemd::Journal::Native.should_receive(:sd_journal_get_data).and_return(-1)
145
+
146
+ j = Systemd::Journal.new
147
+ expect{ j.read_field(:message) }.to raise_error(Systemd::JournalError)
148
+ end
149
+
150
+ it 'parses the returned value correctly.' do
151
+ j = Systemd::Journal.new
152
+
153
+ Systemd::Journal::Native.should_receive(:sd_journal_get_data) do |ptr, field, out_ptr, len_ptr|
154
+ dummy = "MESSAGE=hello world"
155
+ out_ptr.write_pointer(FFI::MemoryPointer.from_string(dummy))
156
+ len_ptr.write_size_t(dummy.size)
157
+ 0
158
+ end
159
+
160
+ j.read_field(:message).should eq("hello world")
161
+ end
101
162
  end
102
163
 
103
164
  describe '#current_entry' do
104
- pending
165
+ before(:each) do
166
+ Systemd::Journal::Native.should_receive(:sd_journal_restart_data).and_return(nil)
167
+ end
168
+
169
+ it 'raises an exception if the call fails' do
170
+ j = Systemd::Journal.new
171
+ Systemd::Journal::Native.should_receive(:sd_journal_enumerate_data).and_return(-1)
172
+ expect { j.current_entry }.to raise_error(Systemd::JournalError)
173
+ end
174
+
175
+ it 'returns the correct data' do
176
+ j = Systemd::Journal.new
177
+ results = ['_PID=100', 'MESSAGE=hello world']
178
+
179
+ Systemd::Journal::Native.should_receive(:sd_journal_enumerate_data).exactly(3).times do |ptr, out_ptr, len_ptr|
180
+ if results.any?
181
+ x = results.shift
182
+ out_ptr.write_pointer(FFI::MemoryPointer.from_string(x))
183
+ len_ptr.write_size_t(x.length)
184
+ 1
185
+ else
186
+ 0
187
+ end
188
+ end
189
+
190
+ entry = j.current_entry
191
+
192
+ entry._pid.should eq('100')
193
+ entry.message.should eq('hello world')
194
+
195
+ end
105
196
  end
106
197
 
107
198
  describe '#query_unique' do
108
- pending
199
+ before(:each) do
200
+ Systemd::Journal::Native.should_receive(:sd_journal_restart_unique).and_return(nil)
201
+ end
202
+
203
+ it 'raises an exception if the call fails' do
204
+ j = Systemd::Journal.new
205
+ Systemd::Journal::Native.should_receive(:sd_journal_query_unique).and_return(-1)
206
+ expect { j.query_unique(:_pid) }.to raise_error(Systemd::JournalError)
207
+ end
208
+
209
+ it 'raises an exception if the call fails (2)' do
210
+ j = Systemd::Journal.new
211
+ Systemd::Journal::Native.should_receive(:sd_journal_query_unique).and_return(0)
212
+ Systemd::Journal::Native.should_receive(:sd_journal_enumerate_unique).and_return(-1)
213
+ expect { j.query_unique(:_pid) }.to raise_error(Systemd::JournalError)
214
+ end
215
+
216
+ it 'returns the correct data' do
217
+ j = Systemd::Journal.new
218
+ results = ['_PID=100', '_PID=200', '_PID=300']
219
+
220
+ Systemd::Journal::Native.should_receive(:sd_journal_query_unique).and_return(0)
221
+
222
+ Systemd::Journal::Native.should_receive(:sd_journal_enumerate_unique).exactly(4).times do |ptr, out_ptr, len_ptr|
223
+ if results.any?
224
+ x = results.shift
225
+ out_ptr.write_pointer(FFI::MemoryPointer.from_string(x))
226
+ len_ptr.write_size_t(x.length)
227
+ 1
228
+ else
229
+ 0
230
+ end
231
+ end
232
+
233
+ j.query_unique(:_pid).should eq(['100', '200', '300'])
234
+ end
235
+
109
236
  end
110
237
 
111
238
  describe '#wait' do
112
- pending
239
+ it 'raises an exception if the call fails' do
240
+ Systemd::Journal::Native.should_receive(:sd_journal_wait).and_return(-1)
241
+
242
+ j = Systemd::Journal.new
243
+ expect{ j.wait(100) }.to raise_error(Systemd::JournalError)
244
+ end
245
+
246
+ it 'returns the reason we were woken up' do
247
+ j = Systemd::Journal.new
248
+ Systemd::Journal::Native.should_receive(:sd_journal_wait).and_return(:append)
249
+ j.wait(100).should eq(:append)
250
+ end
251
+
252
+ it 'returns nil if we reached the timeout.' do
253
+ j = Systemd::Journal.new
254
+ Systemd::Journal::Native.should_receive(:sd_journal_wait).and_return(:nop)
255
+ j.wait(100).should eq(nil)
256
+ end
257
+ end
258
+
259
+ describe '#add_filter' do
260
+ it 'raises an exception if the call fails' do
261
+ Systemd::Journal::Native.should_receive(:sd_journal_add_match).and_return(-1)
262
+
263
+ j = Systemd::Journal.new
264
+ expect{ j.add_filter(:message, "test") }.to raise_error(Systemd::JournalError)
265
+ end
266
+
267
+ it 'formats the arguments appropriately' do
268
+ Systemd::Journal::Native.should_receive(:sd_journal_add_match).
269
+ with(anything, "MESSAGE=test", "MESSAGE=test".length).
270
+ and_return(0)
271
+
272
+ Systemd::Journal.new.add_filter(:message, "test")
273
+ end
113
274
  end
114
275
 
115
- describe '#add_match' do
116
- pending
276
+ describe '#add_filters' do
277
+ it 'calls add_filter for each parameter' do
278
+ j = Systemd::Journal.new
279
+ j.should_receive(:add_filter).with(:priority, 1)
280
+ j.should_receive(:add_filter).with(:_exe, '/usr/bin/sshd')
281
+
282
+ j.add_filters(priority: 1, _exe: '/usr/bin/sshd')
283
+ end
284
+
285
+ it 'expands array arguments to multiple add_filter calls' do
286
+ j = Systemd::Journal.new
287
+ j.should_receive(:add_filter).with(:priority, 1)
288
+ j.should_receive(:add_filter).with(:priority, 2)
289
+ j.should_receive(:add_filter).with(:priority, 3)
290
+
291
+ j.add_filters(priority: [1,2,3])
292
+ end
293
+ end
294
+
295
+ describe '#filter' do
296
+ it 'clears the existing filters' do
297
+ j = Systemd::Journal.new
298
+ j.should_receive(:clear_filters)
299
+
300
+ j.filter({})
301
+ end
302
+
303
+ it 'adds disjunctions between terms' do
304
+ j = Systemd::Journal.new
305
+ j.stub(:clear_filters).and_return(nil)
306
+
307
+ j.should_receive(:add_filter).with(:priority, 1).ordered
308
+ j.should_receive(:add_disjunction).ordered
309
+ j.should_receive(:add_filter).with(:message, 'hello').ordered
310
+
311
+ j.filter({priority: 1}, {message: 'hello'})
312
+
313
+ end
117
314
  end
118
315
 
119
316
  describe '#add_conjunction' do
120
- pending
317
+ it 'raises an exception if the call fails' do
318
+ Systemd::Journal::Native.should_receive(:sd_journal_add_conjunction).and_return(-1)
319
+
320
+ j = Systemd::Journal.new
321
+ expect{ j.add_conjunction }.to raise_error(Systemd::JournalError)
322
+ end
121
323
  end
122
324
 
123
325
  describe '#add_disjunction' do
124
- pending
326
+ it 'raises an exception if the call fails' do
327
+ Systemd::Journal::Native.should_receive(:sd_journal_add_disjunction).and_return(-1)
328
+
329
+ j = Systemd::Journal.new
330
+ expect{ j.add_disjunction }.to raise_error(Systemd::JournalError)
331
+ end
125
332
  end
126
333
 
127
- describe '#clear_matches' do
128
- pending
334
+ describe '#clear_filters' do
335
+ it 'flushes the matches' do
336
+ j = Systemd::Journal.new
337
+ Systemd::Journal::Native.should_receive(:sd_journal_flush_matches).and_return(nil)
338
+ j.clear_filters
339
+ end
129
340
  end
130
341
 
131
342
  describe '#disk_usage' do
132
343
  it 'returns the size used on disk' do
133
344
  Systemd::Journal::Native.should_receive(:sd_journal_get_usage) do |ptr, size_ptr|
134
- size_ptr.size == 8 ? size_ptr.write_uint64(12) : size_ptr.write_uint32(12)
345
+ size_ptr.write_size_t(12)
135
346
  0
136
347
  end
137
348
  j = Systemd::Journal.new
@@ -145,4 +356,95 @@ describe Systemd::Journal do
145
356
  end
146
357
  end
147
358
 
359
+ describe '#data_threshold=' do
360
+ it 'sets the data threshold' do
361
+ j = Systemd::Journal.new
362
+
363
+ Systemd::Journal::Native.should_receive(:sd_journal_set_data_threshold).
364
+ with(anything, 0x1234).and_return(0)
365
+
366
+ j.data_threshold = 0x1234
367
+ end
368
+
369
+ it 'raises a JournalError on failure' do
370
+ j = Systemd::Journal.new
371
+
372
+ Systemd::Journal::Native.should_receive(:sd_journal_set_data_threshold).
373
+ with(anything, 0x1234).and_return(-1)
374
+
375
+ expect { j.data_threshold = 0x1234 }.to raise_error(Systemd::JournalError)
376
+ end
377
+ end
378
+
379
+ describe '#data_threshold' do
380
+ it 'gets the data threshold' do
381
+ j = Systemd::Journal.new
382
+
383
+ Systemd::Journal::Native.should_receive(:sd_journal_get_data_threshold) do |ptr, size_ptr|
384
+ size_ptr.write_size_t(0x1234)
385
+ 0
386
+ end
387
+ j.data_threshold.should eq(0x1234)
388
+ end
389
+
390
+ it 'raises a JournalError on failure' do
391
+ j = Systemd::Journal.new
392
+
393
+ Systemd::Journal::Native.should_receive(:sd_journal_get_data_threshold).
394
+ and_return(-3)
395
+
396
+ expect{ j.data_threshold }.to raise_error(Systemd::JournalError)
397
+ end
398
+
399
+ end
400
+
401
+ describe '#cursor?' do
402
+ it 'returns true if the current cursor is the provided value' do
403
+ j = Systemd::Journal.new
404
+ Systemd::Journal::Native.should_receive(:sd_journal_test_cursor).
405
+ with(anything, "1234").and_return(1)
406
+
407
+ j.cursor?("1234").should eq(true)
408
+ end
409
+
410
+ it 'returns false otherwise' do
411
+ j = Systemd::Journal.new
412
+ Systemd::Journal::Native.should_receive(:sd_journal_test_cursor).
413
+ with(anything, "1234").and_return(0)
414
+
415
+ j.cursor?("1234").should eq(false)
416
+ end
417
+
418
+ it 'raises a JournalError on failure' do
419
+ j = Systemd::Journal.new
420
+
421
+ Systemd::Journal::Native.should_receive(:sd_journal_test_cursor).
422
+ and_return(-3)
423
+
424
+ expect{ j.cursor?('123') }.to raise_error(Systemd::JournalError)
425
+ end
426
+
427
+ end
428
+
429
+ describe '#cursor' do
430
+ it 'returns the current cursor' do
431
+ j = Systemd::Journal.new
432
+ Systemd::Journal::Native.should_receive(:sd_journal_get_cursor) do |ptr, out_ptr|
433
+ out_ptr.write_pointer(FFI::MemoryPointer.from_string("5678"))
434
+ 0
435
+ end
436
+ j.cursor.should eq("5678")
437
+ end
438
+
439
+ it 'raises a JournalError on failure' do
440
+ j = Systemd::Journal.new
441
+
442
+ Systemd::Journal::Native.should_receive(:sd_journal_get_cursor).
443
+ and_return(-3)
444
+
445
+ expect{ j.cursor }.to raise_error(Systemd::JournalError)
446
+ end
447
+
448
+ end
449
+
148
450
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: systemd-journal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Ledbetter
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-01 00:00:00.000000000 Z
12
+ date: 2013-11-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ffi
@@ -69,6 +69,7 @@ files:
69
69
  - examples/journal_directory.rb
70
70
  - examples/ssh_watcher.rb
71
71
  - lib/systemd-journal.rb
72
+ - lib/systemd/ffi_size_t.rb
72
73
  - lib/systemd/id128.rb
73
74
  - lib/systemd/journal.rb
74
75
  - lib/systemd/journal/compat.rb
@@ -76,9 +77,12 @@ files:
76
77
  - lib/systemd/journal/flags.rb
77
78
  - lib/systemd/journal/native.rb
78
79
  - lib/systemd/journal/version.rb
80
+ - lib/systemd/journal_entry.rb
79
81
  - lib/systemd/journal_error.rb
82
+ - spec/no_ffi.rb
80
83
  - spec/spec_helper.rb
81
84
  - spec/systemd/id128_spec.rb
85
+ - spec/systemd/journal_entry_spec.rb
82
86
  - spec/systemd/journal_spec.rb
83
87
  - systemd-journal.gemspec
84
88
  homepage: https://github.com/ledbettj/systemd-journal
@@ -106,7 +110,9 @@ signing_key:
106
110
  specification_version: 4
107
111
  summary: Ruby bindings to libsystemd-journal
108
112
  test_files:
113
+ - spec/no_ffi.rb
109
114
  - spec/spec_helper.rb
110
115
  - spec/systemd/id128_spec.rb
116
+ - spec/systemd/journal_entry_spec.rb
111
117
  - spec/systemd/journal_spec.rb
112
118
  has_rdoc: