Module: Roby::DRoby::Logfile

Extended by:
Logger::Hierarchy
Defined in:
lib/roby/droby/logfile.rb,
lib/roby/droby/logfile/index.rb,
lib/roby/droby/logfile/client.rb,
lib/roby/droby/logfile/reader.rb,
lib/roby/droby/logfile/server.rb,
lib/roby/droby/logfile/writer.rb

Overview

Module containing all the logfile-related code

Note that unlike other Roby facilities, this file is not meant to be required directly. Require either logfile/reader or logfile/writer depending on what you need

Defined Under Namespace

Classes: Client, Index, IndexInvalid, IndexMissing, InvalidFileError, InvalidFormatVersion, Reader, Server, TruncatedFileError, UnknownFormatVersion, Writer

Constant Summary collapse

MAGIC_CODE =
"ROBYLOG"
PROLOGUE_SIZE =
MAGIC_CODE.size + 4
FORMAT_VERSION =
5

Class Method Summary collapse

Class Method Details

.decode_one_chunk(chunk) ⇒ Object

Decode a chunk loaded with read_one_chunk



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/roby/droby/logfile.rb', line 161

def self.decode_one_chunk(chunk)
    begin ::Marshal.load_with_missing_constants(chunk)
    rescue ArgumentError => e
        if e.message == "marshal data too short"
            raise TruncatedFileError, "marshal data invalid"
        end

        raise
    end
rescue TypeError => e
    case e.message
    when /^struct Roby::Actions::Models::Action::Argument not compatible/
        Roby::Actions::Models::Action.const_set(
            :Argument,
            Struct.new(:name, :doc, :required, :default)
        )
        retry
    else
        raise e, "#{e.message}, running roby-log repair "\
                 "might repair the file", e.backtrace
    end
rescue Exception => e # rubocop:disable Lint/RescueException
    raise e, "#{e.message}, running roby-log repair "\
             "might repair the file", e.backtrace
end

.guess_version(input) ⇒ Object

Guess the log file format version for the given IO



55
56
57
58
59
60
61
62
63
# File 'lib/roby/droby/logfile.rb', line 55

def self.guess_version(input)
    input.rewind
    return unless (magic = input.read(Logfile::MAGIC_CODE.size))
    return input.read(4)&.unpack1("L<") if magic == Logfile::MAGIC_CODE

    guess_version_from_marshalled_header(input)
ensure
    input.rewind
end

.guess_version_from_marshalled_header(input) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Guess the version of older files that had a single marshalled object as header



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/roby/droby/logfile.rb', line 69

def self.guess_version_from_marshalled_header(input)
    input.rewind
    header =
        begin ::Marshal.load(input) # rubocop:disable Security/MarshalLoad
        rescue TypeError
            return
        end

    case header
    when Hash
        header[:log_format]
    when Symbol
        first_chunk =
            begin ::Marshal.load(input) # rubocop:disable Security/MarshalLoad
            rescue TypeError
                return
            end

        0 if first_chunk.kind_of?(Array)
    when Array
        1 if header[-2] == :cycle_end
    end
ensure
    input.rewind
end

.read_one_chunk(io) ⇒ Object

Load a chunk of data from an event file. buffer, if given, must be a String object that will be used as intermediate buffer in the process



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/roby/droby/logfile.rb', line 139

def self.read_one_chunk(io)
    data_size = io.read(4)
    return unless data_size

    actual_pos = io.tell
    unless (data_size = data_size.unpack1("L<"))
        raise TruncatedFileError,
              "file truncated, expected 4 bytes at #{actual_pos}"
    end

    buffer = io.read(data_size) || ""
    if buffer.size < data_size
        raise TruncatedFileError,
              "truncated file, expected a chunk of size #{data_size} "\
              "at #{actual_pos + 4}, but got only "\
              "#{buffer ? buffer.size : '0'} bytes"
    end

    buffer
end

.read_prologue(io) ⇒ Object

Read a file prologue, validating that it is of the latest file format



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/roby/droby/logfile.rb', line 99

def self.read_prologue(io)
    magic = io.read(MAGIC_CODE.size)
    if magic != MAGIC_CODE
        raise InvalidFileError, "no magic code at beginning of file"
    end

    unless (log_format = io.read(4)&.unpack1("L<"))
        raise InvalidFileError, "file truncated, cannot read prologue"
    end

    validate_format(log_format)
rescue InvalidFormatVersion
    raise
rescue StandardError
    raise unless (format = guess_version(io))

    validate_format(format)
end

.validate_format(format) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Validate the given format version



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/roby/droby/logfile.rb', line 123

def self.validate_format(format)
    if format < FORMAT_VERSION
        raise InvalidFormatVersion,
              "this is an outdated format (#{format}, current is "\
              "#{FORMAT_VERSION}). Please run roby-log upgrade-format"
    elsif format > FORMAT_VERSION
        raise InvalidFormatVersion,
              "this is an unknown format version #{format}: "\
              "expected #{FORMAT_VERSION}. This file can be read "\
              "only by newer versions of Roby"
    end
end

.write_entry(io, chunk) ⇒ Object

Write a single entry in the log file



188
189
190
191
# File 'lib/roby/droby/logfile.rb', line 188

def self.write_entry(io, chunk)
    io.write([chunk.size].pack("L<"))
    io.write chunk
end

.write_header(io, version: FORMAT_VERSION, **options) ⇒ Object

Write a log file header

The created log file will always have FORMAT_VERSION as its version field



47
48
49
50
51
52
# File 'lib/roby/droby/logfile.rb', line 47

def self.write_header(io, version: FORMAT_VERSION, **options)
    write_prologue(io, version: version)
    options = ::Marshal.dump(options)
    io.write [options.size].pack("L<")
    io.write options
end

.write_prologue(io, version: FORMAT_VERSION) ⇒ Object

Write the file’s prologue, specifying the file magic and format version

The version ID can be specified here mostly for testing purposes.



38
39
40
41
# File 'lib/roby/droby/logfile.rb', line 38

def self.write_prologue(io, version: FORMAT_VERSION)
    io.write(MAGIC_CODE)
    io.write([version].pack("L<"))
end