| Index: tools/isolate/trace_inputs.py
|
| diff --git a/tools/isolate/trace_inputs.py b/tools/isolate/trace_inputs.py
|
| index ae3da6f428958f46c6ec6f59ec9bb0227a694aed..4f1d653c4a0bf71bd0b5c011f7b2446f8cb6b4b5 100755
|
| --- a/tools/isolate/trace_inputs.py
|
| +++ b/tools/isolate/trace_inputs.py
|
| @@ -48,6 +48,18 @@ KEY_TRACKED = 'isolate_dependency_tracked'
|
| KEY_UNTRACKED = 'isolate_dependency_untracked'
|
|
|
|
|
| +class TracingFailure(Exception):
|
| + """An exception occured during tracing."""
|
| + def __init__(self, description, pid, line_number, line, *args):
|
| + super(TracingFailure, self).__init__(
|
| + description, pid, line_number, line, *args)
|
| + self.description = description
|
| + self.pid = pid
|
| + self.line_number = line_number
|
| + self.line = line
|
| + self.extra = args
|
| +
|
| +
|
| ## OS-specific functions
|
|
|
| if sys.platform == 'win32':
|
| @@ -124,6 +136,7 @@ if sys.platform == 'win32':
|
| _MAPPING = {}
|
|
|
| def __init__(self):
|
| + """Lazy loads the cache."""
|
| if not self._MAPPING:
|
| # This is related to UNC resolver on windows. Ignore that.
|
| self._MAPPING['\\Device\\Mup'] = None
|
| @@ -132,18 +145,26 @@ if sys.platform == 'win32':
|
| try:
|
| letter = '%s:' % letter
|
| mapped = QueryDosDevice(letter)
|
| - # It can happen. Assert until we see it happens in the wild. In
|
| - # practice, prefer the lower drive letter.
|
| - assert mapped not in self._MAPPING
|
| - if mapped not in self._MAPPING:
|
| + if mapped in self._MAPPING:
|
| + logging.warn(
|
| + ('Two drives: \'%s\' and \'%s\', are mapped to the same disk'
|
| + '. Drive letters are a user-mode concept and the kernel '
|
| + 'traces only have NT path, so all accesses will be '
|
| + 'associated with the first drive letter, independent of the '
|
| + 'actual letter used by the code') % (
|
| + self._MAPPING[mapped], letter))
|
| + else:
|
| self._MAPPING[mapped] = letter
|
| except WindowsError: # pylint: disable=E0602
|
| pass
|
|
|
| - def to_dos(self, path):
|
| - """Converts a native NT path to DOS path."""
|
| + def to_win32(self, path):
|
| + """Converts a native NT path to Win32/DOS compatible path."""
|
| match = re.match(r'(^\\Device\\[a-zA-Z0-9]+)(\\.*)?$', path)
|
| - assert match, path
|
| + if not match:
|
| + raise ValueError(
|
| + 'Can\'t convert %s into a Win32 compatible path' % path,
|
| + path)
|
| if not match.group(1) in self._MAPPING:
|
| # Unmapped partitions may be accessed by windows for the
|
| # fun of it while the test is running. Discard these.
|
| @@ -164,7 +185,10 @@ if sys.platform == 'win32':
|
|
|
| On Windows, removes any leading '\\?\'.
|
| """
|
| - assert isabs(path), path
|
| + if not isabs(path):
|
| + raise ValueError(
|
| + 'Can\'t get native path case for a non-absolute path: %s' % path,
|
| + path)
|
| # Windows used to have an option to turn on case sensitivity on non Win32
|
| # subsystem but that's out of scope here and isn't supported anymore.
|
| # Go figure why GetShortPathName() is needed.
|
| @@ -194,7 +218,10 @@ elif sys.platform == 'darwin':
|
|
|
| def get_native_path_case(path):
|
| """Returns the native path case for an existing file."""
|
| - assert isabs(path), path
|
| + if not isabs(path):
|
| + raise ValueError(
|
| + 'Can\'t get native path case for a non-absolute path: %s' % path,
|
| + path)
|
| # Technically, it's only HFS+ on OSX that is case insensitive. It's the
|
| # default setting on HFS+ but can be changed.
|
| rel_ref, _ = Carbon.File.FSPathMakeRef(path)
|
| @@ -216,7 +243,10 @@ else: # OSes other than Windows and OSX.
|
|
|
| TODO(maruel): This is not strictly true. Implement if necessary.
|
| """
|
| - assert isabs(path), path
|
| + if not isabs(path):
|
| + raise ValueError(
|
| + 'Can\'t get native path case for a non-absolute path: %s' % path,
|
| + path)
|
| # Give up on cygwin, as GetLongPathName() can't be called.
|
| return path
|
|
|
| @@ -292,8 +322,8 @@ def process_quoted_arguments(text):
|
| state = NEED_QUOTE
|
| current_argument = ''
|
| out = []
|
| - for i in text:
|
| - if i == '"':
|
| + for index, char in enumerate(text):
|
| + if char == '"':
|
| if state == NEED_QUOTE:
|
| state = INSIDE_STRING
|
| elif state == INSIDE_STRING:
|
| @@ -302,18 +332,24 @@ def process_quoted_arguments(text):
|
| current_argument = ''
|
| state = NEED_COMMA_OR_DOT
|
| else:
|
| - assert False, text
|
| - elif i == ',':
|
| + raise ValueError(
|
| + 'Can\'t process char at column %d for: %r' % (index, text),
|
| + index,
|
| + text)
|
| + elif char == ',':
|
| if state in (NEED_COMMA_OR_DOT, NEED_COMMA):
|
| state = NEED_SPACE
|
| else:
|
| - assert False, text
|
| - elif i == ' ':
|
| + raise ValueError(
|
| + 'Can\'t process char at column %d for: %r' % (index, text),
|
| + index,
|
| + text)
|
| + elif char == ' ':
|
| if state == NEED_SPACE:
|
| state = NEED_QUOTE
|
| if state == INSIDE_STRING:
|
| - current_argument += i
|
| - elif i == '.':
|
| + current_argument += char
|
| + elif char == '.':
|
| if state == NEED_COMMA_OR_DOT:
|
| # The string is incomplete, this mean the strace -s flag should be
|
| # increased.
|
| @@ -323,15 +359,24 @@ def process_quoted_arguments(text):
|
| elif state == NEED_DOT_3:
|
| state = NEED_COMMA
|
| elif state == INSIDE_STRING:
|
| - current_argument += i
|
| + current_argument += char
|
| else:
|
| - assert False, text
|
| + raise ValueError(
|
| + 'Can\'t process char at column %d for: %r' % (index, text),
|
| + index,
|
| + text)
|
| else:
|
| if state == INSIDE_STRING:
|
| - current_argument += i
|
| + current_argument += char
|
| else:
|
| - assert False, text
|
| - assert state in (NEED_COMMA, NEED_COMMA_OR_DOT)
|
| + raise ValueError(
|
| + 'Can\'t process char at column %d for: %r' % (index, text),
|
| + index,
|
| + text)
|
| + if state not in (NEED_COMMA, NEED_COMMA_OR_DOT):
|
| + raise ValueError(
|
| + 'String is incorrectly terminated: %r' % text,
|
| + text)
|
| return out
|
|
|
|
|
| @@ -347,6 +392,7 @@ class ApiBase(object):
|
| """
|
| def __init__(self, root, pid, initial_cwd, parentid):
|
| """root is a reference to the Context."""
|
| + # Check internal consistency.
|
| assert isinstance(root, ApiBase.Context)
|
| assert isinstance(pid, int), repr(pid)
|
| self.root = weakref.ref(root)
|
| @@ -447,6 +493,7 @@ class Results(object):
|
| # Shouldn't be used normally, only exists to simplify algorithms.
|
| self.nb_files = 1
|
|
|
| + # Check internal consistency.
|
| assert path, path
|
| assert bool(root) != bool(isabs(path)), (root, path)
|
| assert (
|
| @@ -481,6 +528,7 @@ class Results(object):
|
|
|
| def strip_root(self, root):
|
| """Returns a clone of itself with 'root' stripped off."""
|
| + # Check internal consistency.
|
| assert isabs(root) and root.endswith(os.path.sep), root
|
| if not self.full_path.startswith(root):
|
| return None
|
| @@ -515,15 +563,17 @@ class Results(object):
|
| self.pid = pid
|
| self.files = sorted(
|
| (Results.File(None, f) for f in files), key=lambda x: x.path)
|
| - assert len(set(f.path for f in self.files)) == len(self.files), [
|
| - f.path for f in self.files]
|
| - assert isinstance(children, list)
|
| - assert isinstance(self.files, list)
|
| self.children = children
|
| self.executable = executable
|
| self.command = command
|
| self.initial_cwd = initial_cwd
|
|
|
| + # Check internal consistency.
|
| + assert len(set(f.path for f in self.files)) == len(self.files), [
|
| + f.path for f in self.files]
|
| + assert isinstance(self.children, list)
|
| + assert isinstance(self.files, list)
|
| +
|
| @property
|
| def all(self):
|
| for child in self.children:
|
| @@ -762,111 +812,143 @@ class Strace(ApiBase):
|
| # Ignore signals.
|
| return
|
|
|
| - match = self.RE_KILLED.match(line)
|
| - if match:
|
| - self.handle_exit_group(match.group(1), None, None)
|
| - return
|
| -
|
| - match = self.RE_UNFINISHED.match(line)
|
| - if match:
|
| - assert match.group(1) not in self._pending_calls
|
| - self._pending_calls[match.group(1)] = match.group(1) + match.group(2)
|
| - return
|
| -
|
| - match = self.RE_UNAVAILABLE.match(line)
|
| - if match:
|
| - # This usually means a process was killed and a pending call was
|
| - # canceled.
|
| - # TODO(maruel): Look up the last exit_group() trace just above and
|
| - # make sure any self._pending_calls[anything] is properly flushed.
|
| - return
|
| -
|
| - match = self.RE_RESUMED.match(line)
|
| - if match:
|
| - assert match.group(1) in self._pending_calls, self._pending_calls
|
| - pending = self._pending_calls.pop(match.group(1))
|
| - # Reconstruct the line.
|
| - line = pending + match.group(2)
|
| -
|
| - match = self.RE_HEADER.match(line)
|
| - assert match, (self.pid, self._line_number, line)
|
| - if match.group(1) == self.UNNAMED_FUNCTION:
|
| - return
|
| - handler = getattr(self, 'handle_%s' % match.group(1), None)
|
| - assert handler, (self.pid, self._line_number, line)
|
| try:
|
| - return handler(
|
| - match.group(1),
|
| - match.group(2),
|
| - match.group(3))
|
| - except Exception:
|
| - print >> sys.stderr, (self.pid, self._line_number, line)
|
| + match = self.RE_KILLED.match(line)
|
| + if match:
|
| + # Converts a '+++ killied by Foo +++' trace into an exit_group().
|
| + self.handle_exit_group(match.group(1), None)
|
| + return
|
| +
|
| + match = self.RE_UNFINISHED.match(line)
|
| + if match:
|
| + if match.group(1) in self._pending_calls:
|
| + raise TracingFailure(
|
| + 'Found two unfinished calls for the same function',
|
| + None, None, None,
|
| + self._pending_calls)
|
| + self._pending_calls[match.group(1)] = (
|
| + match.group(1) + match.group(2))
|
| + return
|
| +
|
| + match = self.RE_UNAVAILABLE.match(line)
|
| + if match:
|
| + # This usually means a process was killed and a pending call was
|
| + # canceled.
|
| + # TODO(maruel): Look up the last exit_group() trace just above and
|
| + # make sure any self._pending_calls[anything] is properly flushed.
|
| + return
|
| +
|
| + match = self.RE_RESUMED.match(line)
|
| + if match:
|
| + if match.group(1) not in self._pending_calls:
|
| + raise TracingFailure(
|
| + 'Found a resumed call that was not logged as unfinished',
|
| + None, None, None,
|
| + self._pending_calls)
|
| + pending = self._pending_calls.pop(match.group(1))
|
| + # Reconstruct the line.
|
| + line = pending + match.group(2)
|
| +
|
| + match = self.RE_HEADER.match(line)
|
| + if not match:
|
| + raise TracingFailure(
|
| + 'Found an invalid line: %s' % line,
|
| + None, None, None)
|
| + if match.group(1) == self.UNNAMED_FUNCTION:
|
| + return
|
| + handler = getattr(self, 'handle_%s' % match.group(1), None)
|
| + if not handler:
|
| + raise TracingFailure(
|
| + 'Found a unhandled trace: %s' % match.group(1),
|
| + None, None, None)
|
| +
|
| + return handler(match.group(2), match.group(3))
|
| + except TracingFailure, e:
|
| + # Hack in the values since the handler could be a static function.
|
| + e.pid = self.pid
|
| + e.line = line
|
| + e.line_number = self._line_number
|
| + # Re-raise the modified exception.
|
| raise
|
| -
|
| - def handle_chdir(self, _function, args, result):
|
| + except (KeyError, NotImplementedError, ValueError), e:
|
| + raise TracingFailure(
|
| + 'Trace generated a %s exception: %s' % (
|
| + e.__class__.__name__, str(e)),
|
| + self.pid,
|
| + self._line_number,
|
| + line,
|
| + e)
|
| +
|
| + def handle_chdir(self, args, result):
|
| """Updates cwd."""
|
| - assert result.startswith('0'), 'Unexecpected fail: %s' % result
|
| + if not result.startswith('0'):
|
| + return
|
| cwd = self.RE_CHDIR.match(args).group(1)
|
| self.cwd = self.RelativePath(self, cwd)
|
| logging.debug('handle_chdir(%d, %s)' % (self.pid, self.cwd))
|
|
|
| - def handle_clone(self, _function, _args, result):
|
| + def handle_clone(self, _args, result):
|
| """Transfers cwd."""
|
| if result == '? ERESTARTNOINTR (To be restarted)':
|
| return
|
| # Update the other process right away.
|
| childpid = int(result)
|
| child = self.root().get_or_set_proc(childpid)
|
| + if child.parentid is not None or childpid in self.children:
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in process lifetime detection',
|
| + None, None, None)
|
| +
|
| # Copy the cwd object.
|
| child.initial_cwd = self.get_cwd()
|
| - assert child.parentid is None
|
| child.parentid = self.pid
|
| # It is necessary because the logs are processed out of order.
|
| - assert childpid not in self.children
|
| self.children.append(childpid)
|
|
|
| - def handle_close(self, _function, _args, _result):
|
| + def handle_close(self, _args, _result):
|
| pass
|
|
|
| - def handle_execve(self, _function, args, result):
|
| + def handle_execve(self, args, result):
|
| if result != '0':
|
| return
|
| m = self.RE_EXECVE.match(args)
|
| filepath = m.group(1)
|
| - self._handle_file(filepath, result)
|
| + self._handle_file(filepath)
|
| self.executable = self.RelativePath(self.get_cwd(), filepath)
|
| self.command = process_quoted_arguments(m.group(2))
|
|
|
| - def handle_exit_group(self, _function, _args, _result):
|
| + def handle_exit_group(self, _args, _result):
|
| """Removes cwd."""
|
| self.cwd = None
|
|
|
| @staticmethod
|
| - def handle_fork(_function, args, result):
|
| - assert False, (args, result)
|
| + def handle_fork(_args, _result):
|
| + raise NotImplementedError('Unexpected/unimplemented trace fork()')
|
|
|
| - def handle_open(self, _function, args, result):
|
| + def handle_open(self, args, result):
|
| + if result.startswith('-1'):
|
| + return
|
| args = (self.RE_OPEN3.match(args) or self.RE_OPEN2.match(args)).groups()
|
| if 'O_DIRECTORY' in args[1]:
|
| return
|
| - self._handle_file(args[0], result)
|
| + self._handle_file(args[0])
|
|
|
| - def handle_rename(self, _function, args, result):
|
| + def handle_rename(self, args, result):
|
| + if result.startswith('-1'):
|
| + return
|
| args = self.RE_RENAME.match(args).groups()
|
| - self._handle_file(args[0], result)
|
| - self._handle_file(args[1], result)
|
| + self._handle_file(args[0])
|
| + self._handle_file(args[1])
|
|
|
| @staticmethod
|
| - def handle_stat64(_function, args, result):
|
| - assert False, (args, result)
|
| + def handle_stat64(_args, _result):
|
| + raise NotImplementedError('Unexpected/unimplemented trace stat64()')
|
|
|
| @staticmethod
|
| - def handle_vfork(_function, args, result):
|
| - assert False, (args, result)
|
| + def handle_vfork(_args, _result):
|
| + raise NotImplementedError('Unexpected/unimplemented trace vfork()')
|
|
|
| - def _handle_file(self, filepath, result):
|
| - if result.startswith('-1'):
|
| - return
|
| + def _handle_file(self, filepath):
|
| filepath = self.RelativePath(self.get_cwd(), filepath)
|
| self.add_file(filepath)
|
|
|
| @@ -882,25 +964,48 @@ class Strace(ApiBase):
|
| return self.initial_cwd
|
|
|
| def on_line(self, pid, line):
|
| + """Transfers control into the Process.on_line() function."""
|
| self.get_or_set_proc(pid).on_line(line.strip())
|
|
|
| def to_results(self):
|
| """Finds back the root process and verify consistency."""
|
| # TODO(maruel): Absolutely unecessary, fix me.
|
| root = [p for p in self.processes.itervalues() if not p.parentid]
|
| - assert len(root) == 1
|
| + if len(root) != 1:
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in process lifetime detection',
|
| + None,
|
| + None,
|
| + None,
|
| + root)
|
| process = root[0].to_results_process()
|
| - assert sorted(self.processes) == sorted(p.pid for p in process.all)
|
| + if sorted(self.processes) != sorted(p.pid for p in process.all):
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in process lifetime detection',
|
| + None,
|
| + None,
|
| + None,
|
| + sorted(self.processes),
|
| + sorted(p.pid for p in process.all))
|
| return Results(process)
|
|
|
| def get_or_set_proc(self, pid):
|
| """Returns the Context.Process instance for this pid or creates a new one.
|
| """
|
| - assert isinstance(pid, int) and pid
|
| + if not pid or not isinstance(pid, int):
|
| + raise TracingFailure(
|
| + 'Unpexpected value for pid: %r' % pid,
|
| + pid,
|
| + None,
|
| + None,
|
| + pid)
|
| return self.processes.setdefault(pid, self.Process(self, pid))
|
|
|
| @classmethod
|
| def traces(cls):
|
| + """Returns the list of all handled traces to pass this as an argument to
|
| + strace.
|
| + """
|
| prefix = 'handle_'
|
| return [i[len(prefix):] for i in dir(cls.Process) if i.startswith(prefix)]
|
|
|
| @@ -941,7 +1046,6 @@ class Strace(ApiBase):
|
| out = child.communicate()[0]
|
| # Once it's done, write metadata into the log file to be able to follow the
|
| # pid files.
|
| - assert not os.path.isfile(logname)
|
| with open(logname, 'wb') as f:
|
| json.dump(
|
| {
|
| @@ -1322,7 +1426,7 @@ class Dtrace(ApiBase):
|
| '}\n') + cls.D_CODE
|
|
|
| class Context(ApiBase.Context):
|
| - # This is the most common format. index pid function(args)
|
| + # Format: index pid function(args)
|
| RE_HEADER = re.compile(r'^\d+ (\d+) ([a-zA-Z_\-]+)\((.*?)\)$')
|
|
|
| # Arguments parsing.
|
| @@ -1345,27 +1449,61 @@ class Dtrace(ApiBase):
|
| # First process to be started by self._tracer_pid.
|
| self._initial_pid = None
|
| self._initial_cwd = None
|
| + self._line_number = 0
|
|
|
| def on_line(self, line):
|
| + self._line_number += 1
|
| match = self.RE_HEADER.match(line)
|
| - assert match, line
|
| + if not match:
|
| + raise TracingFailure(
|
| + 'Found malformed line: %s' % line,
|
| + None,
|
| + self._line_number,
|
| + line)
|
| fn = getattr(
|
| self,
|
| 'handle_%s' % match.group(2).replace('-', '_'),
|
| self._handle_ignored)
|
| - return fn(
|
| - int(match.group(1)),
|
| - match.group(3))
|
| + # It is guaranteed to succeed because of the regexp. Or at least I thought
|
| + # it would.
|
| + pid = int(match.group(1))
|
| + try:
|
| + return fn(pid, match.group(3))
|
| + except TracingFailure, e:
|
| + # Hack in the values since the handler could be a static function.
|
| + e.pid = pid
|
| + e.line = line
|
| + e.line_number = self._line_number
|
| + # Re-raise the modified exception.
|
| + raise
|
| + except (KeyError, NotImplementedError, ValueError), e:
|
| + raise TracingFailure(
|
| + 'Trace generated a %s exception: %s' % (
|
| + e.__class__.__name__, str(e)),
|
| + pid,
|
| + self._line_number,
|
| + line,
|
| + e)
|
|
|
| def to_results(self):
|
| """Uses self._initial_pid to determine the initial process."""
|
| process = self.processes[self._initial_pid].to_results_process()
|
| - assert sorted(self.processes) == sorted(p.pid for p in process.all), (
|
| - sorted(self.processes), sorted(p.pid for p in process.all))
|
| + # Internal concistency check.
|
| + if sorted(self.processes) != sorted(p.pid for p in process.all):
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in process lifetime detection',
|
| + None,
|
| + None,
|
| + None,
|
| + sorted(self.processes),
|
| + sorted(p.pid for p in process.all))
|
| return Results(process)
|
|
|
| def handle_dtrace_BEGIN(self, pid, args):
|
| - assert not self._tracer_pid and not self._initial_pid
|
| + if self._tracer_pid or self._initial_pid:
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in dtrace_BEGIN log line',
|
| + None, None, None)
|
| self._tracer_pid = pid
|
| self._initial_cwd = self.RE_DTRACE_BEGIN.match(args).group(1)
|
|
|
| @@ -1376,10 +1514,22 @@ class Dtrace(ApiBase):
|
| are child of the traced processes so there is no need to verify the
|
| process hierarchy.
|
| """
|
| - assert pid not in self.processes
|
| + if pid in self.processes:
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in proc_start: %d started two times' %
|
| + pid,
|
| + None, None, None)
|
| match = self.RE_PROC_START.match(args)
|
| + if not match:
|
| + raise TracingFailure(
|
| + 'Failed to parse arguments: %s' % args,
|
| + None, None, None)
|
| ppid = int(match.group(1))
|
| - assert (ppid == self._tracer_pid) != bool(self._initial_pid)
|
| + if (ppid == self._tracer_pid) == bool(self._initial_pid):
|
| + raise TracingFailure(
|
| + ( 'Parent process is _tracer_pid(%d) but _initial_pid(%d) is '
|
| + 'already set') % (self._tracer_pid, self._initial_pid),
|
| + None, None, None)
|
| if not self._initial_pid:
|
| self._initial_pid = pid
|
| ppid = None
|
| @@ -1408,16 +1558,24 @@ class Dtrace(ApiBase):
|
| :)
|
| """
|
| match = self.RE_EXECVE.match(args)
|
| - assert match, args
|
| + if not match:
|
| + raise TracingFailure(
|
| + 'Failed to parse arguments: %s' % args,
|
| + None, None, None)
|
| proc = self.processes[pid]
|
| proc.executable = match.group(1)
|
| proc.command = process_quoted_arguments(match.group(3))
|
| - assert int(match.group(2)) == len(proc.command), args
|
| + if int(match.group(2)) != len(proc.command):
|
| + raise TracingFailure(
|
| + 'Failed to parse execve() arguments: %s' % args,
|
| + None, None, None)
|
|
|
| def handle_chdir(self, pid, args):
|
| """Updates cwd."""
|
| - assert self._tracer_pid
|
| - assert pid in self.processes
|
| + if not self._tracer_pid or pid not in self.processes:
|
| + raise TracingFailure(
|
| + 'Found inconsistency in the log, is it an old log?',
|
| + None, None, None)
|
| cwd = self.RE_CHDIR.match(args).group(1)
|
| if not cwd.startswith('/'):
|
| cwd2 = os.path.join(self.processes[pid].cwd, cwd)
|
| @@ -1432,7 +1590,10 @@ class Dtrace(ApiBase):
|
|
|
| def handle_open(self, pid, args):
|
| match = self.RE_OPEN.match(args)
|
| - assert match, (pid, args)
|
| + if not match:
|
| + raise TracingFailure(
|
| + 'Failed to parse arguments: %s' % args,
|
| + None, None, None)
|
| flag = int(match.group(2), 16)
|
| if self.O_DIRECTORY & flag == self.O_DIRECTORY:
|
| # Ignore directories.
|
| @@ -1441,7 +1602,10 @@ class Dtrace(ApiBase):
|
|
|
| def handle_rename(self, pid, args):
|
| match = self.RE_RENAME.match(args)
|
| - assert match, (pid, args)
|
| + if not match:
|
| + raise TracingFailure(
|
| + 'Failed to parse arguments: %s' % args,
|
| + None, None, None)
|
| self._handle_file(pid, match.group(1))
|
| self._handle_file(pid, match.group(2))
|
|
|
| @@ -1459,7 +1623,7 @@ class Dtrace(ApiBase):
|
| @staticmethod
|
| def _handle_ignored(pid, args):
|
| """Is called for all the event traces that are not handled."""
|
| - assert False, '%d ??(%s)' % (pid, args)
|
| + raise NotImplementedError('Please implement me')
|
|
|
| @staticmethod
|
| def clean_trace(logname):
|
| @@ -1566,15 +1730,16 @@ class Dtrace(ApiBase):
|
| lines = [l for l in logfile if l.strip()]
|
| errors = [l for l in lines if l.startswith('dtrace:')]
|
| if errors:
|
| - print >> sys.stderr, 'Failed to load: %s' % logname
|
| - print >> sys.stderr, '\n'.join(errors)
|
| - assert not errors, errors
|
| + raise TracingFailure(
|
| + 'Found errors in the trace: %s' % '\n'.join(errors),
|
| + None, None, None, logname)
|
| try:
|
| lines = sorted(lines, key=lambda l: int(l.split(' ', 1)[0]))
|
| except ValueError:
|
| - print >> sys.stderr, 'Failed to load: %s' % logname
|
| - print >> sys.stderr, '\n'.join(lines)
|
| - raise
|
| + raise TracingFailure(
|
| + 'Found errors in the trace: %s' % '\n'.join(
|
| + l for l in lines if l.split(' ', 1)[0].isdigit()),
|
| + None, None, None, logname)
|
| with open(logname, 'wb') as logfile:
|
| logfile.write(''.join(lines))
|
|
|
| @@ -1595,6 +1760,29 @@ class LogmanTrace(ApiBase):
|
|
|
| Ignores directories.
|
| """
|
| + # The basic headers.
|
| + EXPECTED_HEADER = [
|
| + u'Event Name',
|
| + u'Type',
|
| + u'Event ID',
|
| + u'Version',
|
| + u'Channel',
|
| + u'Level', # 5
|
| + u'Opcode',
|
| + u'Task',
|
| + u'Keyword',
|
| + u'PID',
|
| + u'TID', # 10
|
| + u'Processor Number',
|
| + u'Instance ID',
|
| + u'Parent Instance ID',
|
| + u'Activity ID',
|
| + u'Related Activity ID', # 15
|
| + u'Clock-Time',
|
| + u'Kernel(ms)', # Both have a resolution of ~15ms which makes them
|
| + u'User(ms)', # pretty much useless.
|
| + u'User Data', # Extra arguments that are event-specific.
|
| + ]
|
| # Only the useful headers common to all entries are listed there. Any column
|
| # at 19 or higher is dependent on the specific event.
|
| EVENT_NAME = 0
|
| @@ -1603,6 +1791,7 @@ class LogmanTrace(ApiBase):
|
| TID = 10
|
| PROCESSOR_ID = 11
|
| TIMESTAMP = 16
|
| + NULL_GUID = '{00000000-0000-0000-0000-000000000000}'
|
|
|
| class Process(ApiBase.Context.Process):
|
| def __init__(self, *args):
|
| @@ -1622,83 +1811,82 @@ class LogmanTrace(ApiBase):
|
| self._initial_pid = None
|
| self._line_number = 0
|
|
|
| - def on_csv_line(self, line):
|
| + def on_csv_line(self, raw_line):
|
| """Processes a CSV Event line."""
|
| # So much white space!
|
| - line = [i.strip() for i in line]
|
| + line = [i.strip() for i in raw_line]
|
| self._line_number += 1
|
| - if self._line_number == 1:
|
| - assert line == [
|
| - u'Event Name',
|
| - u'Type',
|
| - u'Event ID',
|
| - u'Version',
|
| - u'Channel',
|
| - u'Level', # 5
|
| - u'Opcode',
|
| - u'Task',
|
| - u'Keyword',
|
| - u'PID',
|
| - u'TID', # 10
|
| - u'Processor Number',
|
| - u'Instance ID',
|
| - u'Parent Instance ID',
|
| - u'Activity ID',
|
| - u'Related Activity ID', # 15
|
| - u'Clock-Time',
|
| - u'Kernel(ms)', # Both have a resolution of ~15ms which makes them
|
| - u'User(ms)', # pretty much useless.
|
| - u'User Data',
|
| - ]
|
| - return
|
| -
|
| - # As you can see, the CSV is full of useful non-redundant information:
|
| - # Event ID
|
| - assert line[2] == '0'
|
| - # Version
|
| - assert line[3] in ('2', '3'), line[3]
|
| - # Channel
|
| - assert line[4] == '0'
|
| - # Level
|
| - assert line[5] == '0'
|
| - # Task
|
| - assert line[7] == '0'
|
| - # Keyword
|
| - assert line[8] == '0x0000000000000000'
|
| - # Instance ID
|
| - assert line[12] == ''
|
| - # Parent Instance ID
|
| - assert line[13] == ''
|
| - # Activity ID
|
| - assert line[14] == '{00000000-0000-0000-0000-000000000000}'
|
| - # Related Activity ID
|
| - assert line[15] == ''
|
| -
|
| - if line[0].startswith('{'):
|
| - # Skip GUIDs.
|
| - return
|
| + try:
|
| + if self._line_number == 1:
|
| + if line != self.EXPECTED_HEADER:
|
| + raise TracingFailure(
|
| + 'Found malformed header: %s' % ' '.join(line),
|
| + None, None, None)
|
| + return
|
|
|
| - # Convert the PID in-place from hex.
|
| - line[self.PID] = int(line[self.PID], 16)
|
| + # As you can see, the CSV is full of useful non-redundant information:
|
| + if (line[2] != '0' or # Event ID
|
| + line[3] not in ('2', '3') or # Version
|
| + line[4] != '0' or # Channel
|
| + line[5] != '0' or # Level
|
| + line[7] != '0' or # Task
|
| + line[8] != '0x0000000000000000' or # Keyword
|
| + line[12] != '' or # Instance ID
|
| + line[13] != '' or # Parent Instance ID
|
| + line[14] != self.NULL_GUID or # Activity ID
|
| + line[15] != ''): # Related Activity ID
|
| + raise TracingFailure(
|
| + 'Found unexpected values in line: %s' % ' '.join(line),
|
| + None, None, None)
|
| + if line[0].startswith('{'):
|
| + # Skip GUIDs.
|
| + return
|
|
|
| - # By Opcode
|
| - handler = getattr(
|
| - self,
|
| - 'handle_%s_%s' % (line[self.EVENT_NAME], line[self.TYPE]),
|
| - None)
|
| - if not handler:
|
| - # Try to get an universal fallback
|
| - handler = getattr(self, 'handle_%s_Any' % line[self.EVENT_NAME], None)
|
| - if handler:
|
| + # Convert the PID in-place from hex.
|
| + line[self.PID] = int(line[self.PID], 16)
|
| +
|
| + # By Opcode
|
| + handler = getattr(
|
| + self,
|
| + 'handle_%s_%s' % (line[self.EVENT_NAME], line[self.TYPE]),
|
| + None)
|
| + if not handler:
|
| + # Try to get an universal fallback for the event class.
|
| + handler = getattr(self, 'handle_%s_Any' % line[self.EVENT_NAME], None)
|
| + if not handler:
|
| + raise TracingFailure(
|
| + 'Unexpected event %s_%s' % (
|
| + line[self.EVENT_NAME], line[self.TYPE]),
|
| + None, None, None)
|
| handler(line)
|
| - else:
|
| - assert False, '%s_%s' % (line[self.EVENT_NAME], line[self.TYPE])
|
| + except TracingFailure, e:
|
| + # Hack in the values since the handler could be a static function.
|
| + e.pid = line[self.PID]
|
| + e.line = line
|
| + e.line_number = self._line_number
|
| + # Re-raise the modified exception.
|
| + raise
|
| + except (KeyError, NotImplementedError, ValueError), e:
|
| + raise TracingFailure(
|
| + 'Trace generated a %s exception: %s' % (
|
| + e.__class__.__name__, str(e)),
|
| + line[self.PID],
|
| + self._line_number,
|
| + line,
|
| + e)
|
|
|
| def to_results(self):
|
| """Uses self._initial_pid to determine the initial process."""
|
| process = self.processes[self._initial_pid].to_results_process()
|
| - assert sorted(self.processes) == sorted(p.pid for p in process.all), (
|
| - sorted(self.processes), sorted(p.pid for p in process.all))
|
| + # Internal concistency check.
|
| + if sorted(self.processes) != sorted(p.pid for p in process.all):
|
| + raise TracingFailure(
|
| + 'Found internal inconsitency in process lifetime detection',
|
| + None,
|
| + None,
|
| + None,
|
| + sorted(self.processes),
|
| + sorted(p.pid for p in process.all))
|
| return Results(process)
|
|
|
| def _thread_to_process(self, tid):
|
| @@ -1709,7 +1897,7 @@ class LogmanTrace(ApiBase):
|
| @staticmethod
|
| def handle_EventTrace_Header(line):
|
| """Verifies no event was dropped, e.g. no buffer overrun occured."""
|
| - #BUFFER_SIZE = 19
|
| + BUFFER_SIZE = 19
|
| #VERSION = 20
|
| #PROVIDER_VERSION = 21
|
| #NUMBER_OF_PROCESSORS = 22
|
| @@ -1731,7 +1919,11 @@ class LogmanTrace(ApiBase):
|
| #BUFFERS_LOST = 38
|
| #SESSION_NAME_STRING = 39
|
| #LOG_FILE_NAME_STRING = 40
|
| - assert line[EVENTS_LOST] == '0'
|
| + if line[EVENTS_LOST] != '0':
|
| + raise TracingFailure(
|
| + ( 'Events were lost during trace, please increase the buffer size '
|
| + 'from %s') % line[BUFFER_SIZE],
|
| + None, None, None)
|
|
|
| def handle_EventTrace_Any(self, line):
|
| pass
|
| @@ -1786,7 +1978,7 @@ class LogmanTrace(ApiBase):
|
| # Ignore directories and bare drive right away.
|
| if raw_path.endswith(os.path.sep):
|
| return
|
| - filename = self._drive_map.to_dos(raw_path)
|
| + filename = self._drive_map.to_win32(raw_path)
|
| # Ignore bare drive right away. Some may still fall through with format
|
| # like '\\?\X:'
|
| if len(filename) == 2:
|
| @@ -1828,22 +2020,35 @@ class LogmanTrace(ApiBase):
|
| if ppid == self._tracer_pid:
|
| # Need to ignore processes we don't know about because the log is
|
| # system-wide. self._tracer_pid shall start only one process.
|
| - assert not self._initial_pid
|
| + if self._initial_pid:
|
| + raise TracingFailure(
|
| + ( 'Parent process is _tracer_pid(%d) but _initial_pid(%d) is '
|
| + 'already set') % (self._tracer_pid, self._initial_pid),
|
| + None, None, None)
|
| self._initial_pid = pid
|
| ppid = None
|
| elif ppid not in self.processes:
|
| # Ignore
|
| return
|
| - assert pid not in self.processes
|
| + if pid in self.processes:
|
| + raise TracingFailure(
|
| + 'Process %d started two times' % pid,
|
| + None, None, None)
|
| proc = self.processes[pid] = self.Process(self, pid, None, ppid)
|
| +
|
| + if (not line[IMAGE_FILE_NAME].startswith('"') or
|
| + not line[IMAGE_FILE_NAME].endswith('"')):
|
| + raise TracingFailure(
|
| + 'Command line is not properly quoted: %s' % line[IMAGE_FILE_NAME],
|
| + None, None, None)
|
| +
|
| # TODO(maruel): Process escapes.
|
| - assert (
|
| - line[COMMAND_LINE].startswith('"') and
|
| - line[COMMAND_LINE].endswith('"'))
|
| + if (not line[COMMAND_LINE].startswith('"') or
|
| + not line[COMMAND_LINE].endswith('"')):
|
| + raise TracingFailure(
|
| + 'Command line is not properly quoted: %s' % line[COMMAND_LINE],
|
| + None, None, None)
|
| proc.command = CommandLineToArgvW(line[COMMAND_LINE][1:-1])
|
| - assert (
|
| - line[IMAGE_FILE_NAME].startswith('"') and
|
| - line[IMAGE_FILE_NAME].endswith('"'))
|
| proc.executable = line[IMAGE_FILE_NAME][1:-1]
|
| # proc.command[0] may be the absolute path of 'executable' but it may be
|
| # anything else too. If it happens that command[0] ends with executable,
|
| @@ -2020,7 +2225,7 @@ class LogmanTrace(ApiBase):
|
| elif logformat == 'xml':
|
| cmd_convert.extend(['-of', 'XML'])
|
| else:
|
| - assert False, logformat
|
| + raise ValueError('Unexpected log format \'%s\'' % logformat)
|
| logging.debug('Running: %s' % cmd_convert)
|
| # This can takes tens of minutes for large logs.
|
| subprocess.check_call(
|
|
|