diff --git a/devlib/collector/dmesg.py b/devlib/collector/dmesg.py index 40676c7..e6eb9fe 100644 --- a/devlib/collector/dmesg.py +++ b/devlib/collector/dmesg.py @@ -22,6 +22,7 @@ from devlib.collector import (CollectorBase, CollectorOutput, CollectorOutputEntry) from devlib.target import KernelConfigTristate from devlib.exception import TargetStableError +from devlib.utils.misc import memoized class KernelLogEntry(object): @@ -39,20 +40,26 @@ class KernelLogEntry(object): :param msg: Content of the entry :type msg: str + + :param line_nr: Line number at which this entry appeared in the ``dmesg`` + output. Note that this is not guaranteed to be unique across collectors, as + the buffer can be cleared. The timestamp is the only reliable index. + :type line_nr: int """ _TIMESTAMP_MSG_REGEX = re.compile(r'\[(.*?)\] (.*)') _RAW_LEVEL_REGEX = re.compile(r'<([0-9]+)>(.*)') _PRETTY_LEVEL_REGEX = re.compile(r'\s*([a-z]+)\s*:([a-z]+)\s*:\s*(.*)') - def __init__(self, facility, level, timestamp, msg): + def __init__(self, facility, level, timestamp, msg, line_nr=0): self.facility = facility self.level = level self.timestamp = timestamp self.msg = msg + self.line_nr = line_nr @classmethod - def from_str(cls, line): + def from_str(cls, line, line_nr=0): """ Parses a "dmesg --decode" output line, formatted as following: kern :err : [3618282.310743] nouveau 0000:01:00.0: systemd-logind[988]: nv50cal_space: -16 @@ -99,6 +106,7 @@ class KernelLogEntry(object): level=level, timestamp=timestamp, msg=msg.strip(), + line_nr=line_nr, ) @classmethod @@ -110,9 +118,9 @@ class KernelLogEntry(object): .. note:: The same restrictions on the dmesg output format as for :meth:`from_str` apply. """ - for line in dmesg_out.splitlines(): + for i, line in enumerate(dmesg_out.splitlines()): if line.strip(): - yield cls.from_str(line) + yield cls.from_str(line, line_nr=i) def __str__(self): facility = self.facility + ': ' if self.facility else '' @@ -135,6 +143,11 @@ class DmesgCollector(CollectorBase): :param facility: Facility to record, see dmesg --help for the list. :type level: str + :param empty_buffer: If ``True``, the kernel dmesg ring buffer will be + emptied before starting. Note that this will break nesting of collectors, + so it's not recommended unless it's really necessary. + :type empty_buffer: bool + .. warning:: If BusyBox dmesg is used, facility and level will be ignored, and the parsed entries will also lack that information. """ @@ -152,7 +165,7 @@ class DmesgCollector(CollectorBase): "debug", # debug-level messages ] - def __init__(self, target, level=LOG_LEVELS[-1], facility='kern'): + def __init__(self, target, level=LOG_LEVELS[-1], facility='kern', empty_buffer=False): super(DmesgCollector, self).__init__(target) if not target.is_rooted: @@ -175,19 +188,72 @@ class DmesgCollector(CollectorBase): self.facility = facility self.needs_root = bool(target.config.typed_config.get( 'CONFIG_SECURITY_DMESG_RESTRICT', KernelConfigTristate.NO)) - self.reset() + + self._begin_timestamp = None + self.empty_buffer = empty_buffer + self._dmesg_out = None + + @property + def dmesg_out(self): + out = self._dmesg_out + if out is None: + return None + else: + try: + entry = self.entries[0] + except IndexError: + i = 0 + else: + i = entry.line_nr + return '\n'.join(out.splitlines()[i:]) @property def entries(self): - return KernelLogEntry.from_dmesg_output(self.dmesg_out) + return self._get_entries(self._dmesg_out, self._begin_timestamp) + + @memoized + def _get_entries(self, dmesg_out, timestamp): + entries = KernelLogEntry.from_dmesg_output(dmesg_out) + entries = list(entries) + if timestamp is None: + return entries + else: + try: + first = entries[0] + except IndexError: + pass + else: + if first.timestamp > timestamp: + msg = 'The dmesg ring buffer has ran out of memory or has been cleared and some entries have been lost' + raise ValueError(msg) + + return [ + entry + for entry in entries + # Only select entries that are more recent than the one at last + # reset() + if entry.timestamp > timestamp + ] def reset(self): - self.dmesg_out = None + # If the buffer is emptied on start(), it does not matter as we will + # not end up with entries dating from before start() + if self.empty_buffer: + # Empty the dmesg ring buffer. This requires root in all cases + self.target.execute('dmesg -c', as_root=True) + else: + self.stop() + try: + entry = self.entries[-1] + except IndexError: + pass + else: + self._begin_timestamp = entry.timestamp + + self._dmesg_out = None def start(self): self.reset() - # Empty the dmesg ring buffer. This requires root in all cases - self.target.execute('dmesg -c', as_root=True) def stop(self): levels_list = list(takewhile( @@ -203,7 +269,7 @@ class DmesgCollector(CollectorBase): facility=self.facility, ) - self.dmesg_out = self.target.execute(cmd, as_root=self.needs_root) + self._dmesg_out = self.target.execute(cmd, as_root=self.needs_root) def set_output(self, output_path): self.output_path = output_path