blob: acdeada2e4648440cccfc5335190a5901fd77126 [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
2# coding=utf-8
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05003# Copyright 2012 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05004# Use of this source code is governed under the Apache License, Version 2.0 that
5# can be found in the LICENSE file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00006
7"""Traces an executable and its child processes and extract the files accessed
8by them.
9
10The implementation uses OS-specific API. The native Kernel logger and the ETL
11interface is used on Windows. Dtrace is used on OSX. Strace is used otherwise.
12The OS-specific implementation is hidden in an 'API' interface.
13
14The results are embedded in a Results instance. The tracing is done in two
15phases, the first is to do the actual trace and generate an
16implementation-specific log file. Then the log file is parsed to extract the
17information, including the individual child processes and the files accessed
18from the log.
19"""
20
21import codecs
22import csv
maruel@chromium.org9e56b592013-05-21 16:56:05 +000023import errno
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000024import getpass
25import glob
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000026import logging
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000027import os
28import re
maruel@chromium.org9b2112a2013-04-05 18:01:02 +000029import stat
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000030import subprocess
31import sys
32import tempfile
33import threading
34import time
35import weakref
36
maruel@chromium.orge5322512013-08-19 20:17:57 +000037from third_party import colorama
38from third_party.depot_tools import fix_encoding
39from third_party.depot_tools import subcommand
40
maruel@chromium.org561d4b22013-09-26 21:08:08 +000041from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000042from utils import tools
43
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000044## OS-specific imports
45
46if sys.platform == 'win32':
maruel@chromium.org561d4b22013-09-26 21:08:08 +000047 from ctypes.wintypes import byref, c_int, c_wchar_p
48 from ctypes.wintypes import windll # pylint: disable=E0611
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000049
50
maruel@chromium.org3d671992013-08-20 00:38:27 +000051__version__ = '0.1'
52
53
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000054BASE_DIR = os.path.dirname(os.path.abspath(__file__))
55ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR))
56
57
58class TracingFailure(Exception):
59 """An exception occured during tracing."""
60 def __init__(self, description, pid, line_number, line, *args):
61 super(TracingFailure, self).__init__(
62 description, pid, line_number, line, *args)
63 self.description = description
64 self.pid = pid
65 self.line_number = line_number
66 self.line = line
67 self.extra = args
68
69 def __str__(self):
70 out = self.description
71 if self.pid:
72 out += '\npid: %d' % self.pid
73 if self.line_number:
74 out += '\nline: %d' % self.line_number
75 if self.line:
76 out += '\n%s' % self.line
77 if self.extra:
78 out += '\n' + ', '.join(map(str, filter(None, self.extra)))
79 return out
80
81
82## OS-specific functions
83
84if sys.platform == 'win32':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000085 def get_current_encoding():
86 """Returns the 'ANSI' code page associated to the process."""
87 return 'cp%d' % int(windll.kernel32.GetACP())
88
89
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000090 def CommandLineToArgvW(command_line):
91 """Splits a commandline into argv using CommandLineToArgvW()."""
92 # http://msdn.microsoft.com/library/windows/desktop/bb776391.aspx
93 size = c_int()
maruel@chromium.org306e0e72012-11-02 18:22:03 +000094 assert isinstance(command_line, unicode)
95 ptr = windll.shell32.CommandLineToArgvW(command_line, byref(size))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000096 try:
97 return [arg for arg in (c_wchar_p * size.value).from_address(ptr)]
98 finally:
99 windll.kernel32.LocalFree(ptr)
100
101
Marc-Antoine Ruel5906fcd2013-11-08 21:02:24 -0500102def can_trace():
103 """Returns True if the user is an administrator on Windows.
104
105 It is required for tracing to work.
106 """
107 if sys.platform != 'win32':
108 return True
109 return bool(windll.shell32.IsUserAnAdmin())
110
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000111
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000112def create_subprocess_thunk():
113 """Creates a small temporary script to start the child process.
114
115 This thunk doesn't block, its unique name is used to identify it as the
116 parent.
117 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000118 handle, name = tempfile.mkstemp(prefix='trace_inputs_thunk', suffix='.py')
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000119 try:
120 os.write(
121 handle,
122 (
123 'import subprocess, sys\n'
124 'sys.exit(subprocess.call(sys.argv[2:]))\n'
125 ))
126 finally:
127 os.close(handle)
128 return name
129
130
131def create_exec_thunk():
132 """Creates a small temporary script to start the child executable.
133
134 Reads from the file handle provided as the fisrt argument to block, then
135 execv() the command to be traced.
136 """
137 handle, name = tempfile.mkstemp(prefix='trace_inputs_thunk', suffix='.py')
138 try:
139 os.write(
140 handle,
141 (
142 'import os, sys\n'
143 'fd = int(sys.argv[1])\n'
144 # This will block until the controlling process writes a byte on the
145 # pipe. It will do so once the tracing tool, e.g. strace, is ready to
146 # trace.
147 'os.read(fd, 1)\n'
148 'os.close(fd)\n'
149 'os.execv(sys.argv[2], sys.argv[2:])\n'
150 ))
151 finally:
152 os.close(handle)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000153 return name
154
155
156def strace_process_quoted_arguments(text):
157 """Extracts quoted arguments on a string and return the arguments as a list.
158
159 Implemented as an automaton. Supports incomplete strings in the form
160 '"foo"...'.
161
162 Example:
163 With text = '"foo", "bar"', the function will return ['foo', 'bar']
164
165 TODO(maruel): Implement escaping.
166 """
167 # All the possible states of the DFA.
168 ( NEED_QUOTE, # Begining of a new arguments.
169 INSIDE_STRING, # Inside an argument.
170 ESCAPED, # Found a '\' inside a quote. Treat the next char as-is.
171 NEED_COMMA_OR_DOT, # Right after the closing quote of an argument. Could be
172 # a serie of 3 dots or a comma.
173 NEED_SPACE, # Right after a comma
174 NEED_DOT_2, # Found a dot, need a second one.
175 NEED_DOT_3, # Found second dot, need a third one.
176 NEED_COMMA, # Found third dot, need a comma.
177 ) = range(8)
178
179 state = NEED_QUOTE
180 out = []
181 for index, char in enumerate(text):
182 if char == '"':
183 if state == NEED_QUOTE:
184 state = INSIDE_STRING
185 # A new argument was found.
186 out.append('')
187 elif state == INSIDE_STRING:
188 # The argument is now closed.
189 state = NEED_COMMA_OR_DOT
190 elif state == ESCAPED:
191 out[-1] += char
192 state = INSIDE_STRING
193 else:
194 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000195 'Can\'t process char \'%s\' at column %d for: %r' % (
196 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000197 index,
198 text)
199 elif char == ',':
200 if state in (NEED_COMMA_OR_DOT, NEED_COMMA):
201 state = NEED_SPACE
202 elif state == INSIDE_STRING:
203 out[-1] += char
204 elif state == ESCAPED:
205 out[-1] += char
206 state = INSIDE_STRING
207 else:
208 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000209 'Can\'t process char \'%s\' at column %d for: %r' % (
210 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000211 index,
212 text)
213 elif char == ' ':
214 if state == NEED_SPACE:
215 state = NEED_QUOTE
216 elif state == INSIDE_STRING:
217 out[-1] += char
218 elif state == ESCAPED:
219 out[-1] += char
220 state = INSIDE_STRING
221 else:
222 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000223 'Can\'t process char \'%s\' at column %d for: %r' % (
224 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000225 index,
226 text)
227 elif char == '.':
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000228 if state in (NEED_QUOTE, NEED_COMMA_OR_DOT):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000229 # The string is incomplete, this mean the strace -s flag should be
230 # increased.
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000231 # For NEED_QUOTE, the input string would look like '"foo", ...'.
232 # For NEED_COMMA_OR_DOT, the input string would look like '"foo"...'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000233 state = NEED_DOT_2
234 elif state == NEED_DOT_2:
235 state = NEED_DOT_3
236 elif state == NEED_DOT_3:
237 state = NEED_COMMA
238 elif state == INSIDE_STRING:
239 out[-1] += char
240 elif state == ESCAPED:
241 out[-1] += char
242 state = INSIDE_STRING
243 else:
244 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000245 'Can\'t process char \'%s\' at column %d for: %r' % (
246 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000247 index,
248 text)
249 elif char == '\\':
250 if state == ESCAPED:
251 out[-1] += char
252 state = INSIDE_STRING
253 elif state == INSIDE_STRING:
254 state = ESCAPED
255 else:
256 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000257 'Can\'t process char \'%s\' at column %d for: %r' % (
258 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000259 index,
260 text)
261 else:
262 if state == INSIDE_STRING:
263 out[-1] += char
264 else:
265 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000266 'Can\'t process char \'%s\' at column %d for: %r' % (
267 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000268 index,
269 text)
270 if state not in (NEED_COMMA, NEED_COMMA_OR_DOT):
271 raise ValueError(
272 'String is incorrectly terminated: %r' % text,
273 text)
274 return out
275
276
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000277def assert_is_renderable(pseudo_string):
278 """Asserts the input is a valid object to be processed by render()."""
279 assert (
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000280 pseudo_string is None or
281 isinstance(pseudo_string, unicode) or
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000282 hasattr(pseudo_string, 'render')), repr(pseudo_string)
283
284
285def render(pseudo_string):
286 """Converts the pseudo-string to an unicode string."""
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000287 if pseudo_string is None or isinstance(pseudo_string, unicode):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000288 return pseudo_string
289 return pseudo_string.render()
290
291
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000292class Results(object):
293 """Results of a trace session."""
294
295 class _TouchedObject(object):
296 """Something, a file or a directory, that was accessed."""
297 def __init__(self, root, path, tainted, size, nb_files):
298 logging.debug(
299 '%s(%s, %s, %s, %s, %s)' %
300 (self.__class__.__name__, root, path, tainted, size, nb_files))
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000301 assert_is_renderable(root)
302 assert_is_renderable(path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000303 self.root = root
304 self.path = path
305 self.tainted = tainted
306 self.nb_files = nb_files
maruel@chromium.org538141b2013-06-03 20:57:17 +0000307 # Can be used as a cache or a default value, depending on context. In
308 # particular, once self.tainted is True, because the path was replaced
309 # with a variable, it is not possible to look up the file size.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000310 self._size = size
311 # These are cache only.
312 self._real_path = None
313
314 # Check internal consistency.
315 assert path, path
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000316 assert tainted or bool(root) != bool(file_path.isabs(path)), (root, path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000317 assert tainted or (
318 not os.path.exists(self.full_path) or
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000319 (self.full_path == file_path.get_native_path_case(self.full_path))), (
320 tainted,
321 self.full_path,
322 file_path.get_native_path_case(self.full_path))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000323
324 @property
325 def existent(self):
326 return self.size != -1
327
328 @property
329 def full_path(self):
330 if self.root:
331 return os.path.join(self.root, self.path)
332 return self.path
333
334 @property
335 def real_path(self):
336 """Returns the path with symlinks resolved."""
337 if not self._real_path:
338 self._real_path = os.path.realpath(self.full_path)
339 return self._real_path
340
341 @property
342 def size(self):
maruel@chromium.org538141b2013-06-03 20:57:17 +0000343 """File's size. -1 is not existent.
344
345 Once tainted, it is not possible the retrieve the file size anymore since
346 the path is composed of variables.
347 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000348 if self._size is None and not self.tainted:
349 try:
350 self._size = os.stat(self.full_path).st_size
351 except OSError:
352 self._size = -1
353 return self._size
354
355 def flatten(self):
356 """Returns a dict representing this object.
357
358 A 'size' of 0 means the file was only touched and not read.
359 """
360 return {
361 'path': self.path,
362 'size': self.size,
363 }
364
365 def replace_variables(self, variables):
366 """Replaces the root of this File with one of the variables if it matches.
367
368 If a variable replacement occurs, the cloned object becomes tainted.
369 """
370 for variable, root_path in variables.iteritems():
371 if self.path.startswith(root_path):
372 return self._clone(
373 self.root, variable + self.path[len(root_path):], True)
374 # No need to clone, returns ourself.
375 return self
376
377 def strip_root(self, root):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000378 """Returns a clone of itself with 'root' stripped off.
379
380 Note that the file is kept if it is either accessible from a symlinked
381 path that was used to access the file or through the real path.
382 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000383 # Check internal consistency.
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000384 assert (
385 self.tainted or
386 (file_path.isabs(root) and root.endswith(os.path.sep))), root
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000387 if not self.full_path.startswith(root):
388 # Now try to resolve the symlinks to see if it can be reached this way.
389 # Only try *after* trying without resolving symlink.
390 if not self.real_path.startswith(root):
391 return None
392 path = self.real_path
393 else:
394 path = self.full_path
395 return self._clone(root, path[len(root):], self.tainted)
396
397 def _clone(self, new_root, new_path, tainted):
398 raise NotImplementedError(self.__class__.__name__)
399
400 class File(_TouchedObject):
401 """A file that was accessed. May not be present anymore.
402
403 If tainted is true, it means it is not a real path anymore as a variable
404 replacement occured.
405
maruel@chromium.org538141b2013-06-03 20:57:17 +0000406 |mode| can be one of None, TOUCHED, READ or WRITE.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000407 """
maruel@chromium.org538141b2013-06-03 20:57:17 +0000408 # Was probed for existence, and it is existent, but was never _opened_.
409 TOUCHED = 't'
410 # Opened for read only and guaranteed to not have been written to.
411 READ = 'r'
412 # Opened for write.
413 WRITE = 'w'
414
415 # They are listed in order of priority. E.g. if a file is traced as TOUCHED
416 # then as WRITE, only keep WRITE. None means no idea, which is a problem on
417 # Windows.
418 ACCEPTABLE_MODES = (None, TOUCHED, READ, WRITE)
419
420 def __init__(self, root, path, tainted, size, mode):
421 assert mode in self.ACCEPTABLE_MODES
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000422 super(Results.File, self).__init__(root, path, tainted, size, 1)
maruel@chromium.org538141b2013-06-03 20:57:17 +0000423 self.mode = mode
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000424
425 def _clone(self, new_root, new_path, tainted):
426 """Clones itself keeping meta-data."""
427 # Keep the self.size and self._real_path caches for performance reason. It
428 # is also important when the file becomes tainted (with a variable instead
429 # of the real path) since self.path is not an on-disk path anymore so
430 # out._size cannot be updated.
maruel@chromium.org538141b2013-06-03 20:57:17 +0000431 out = self.__class__(new_root, new_path, tainted, self.size, self.mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000432 out._real_path = self._real_path
433 return out
434
maruel@chromium.org538141b2013-06-03 20:57:17 +0000435 def flatten(self):
436 out = super(Results.File, self).flatten()
437 out['mode'] = self.mode
438 return out
439
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000440 class Directory(_TouchedObject):
maruel@chromium.org538141b2013-06-03 20:57:17 +0000441 """A directory of files. Must exist.
442
443 For a Directory instance, self.size is not a cache, it's an actual value
444 that is never modified and represents the total size of the files contained
445 in this directory. It is possible that the directory is empty so that
446 size==0; this happens if there's only an invalid symlink in it.
447 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000448 def __init__(self, root, path, tainted, size, nb_files):
449 """path='.' is a valid value and must be handled appropriately."""
450 assert not path.endswith(os.path.sep), path
451 super(Results.Directory, self).__init__(
452 root, path + os.path.sep, tainted, size, nb_files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000453
454 def flatten(self):
455 out = super(Results.Directory, self).flatten()
456 out['nb_files'] = self.nb_files
457 return out
458
459 def _clone(self, new_root, new_path, tainted):
460 """Clones itself keeping meta-data."""
461 out = self.__class__(
462 new_root,
463 new_path.rstrip(os.path.sep),
464 tainted,
465 self.size,
466 self.nb_files)
467 out._real_path = self._real_path
468 return out
469
470 class Process(object):
471 """A process that was traced.
472
473 Contains references to the files accessed by this process and its children.
474 """
475 def __init__(self, pid, files, executable, command, initial_cwd, children):
476 logging.debug('Process(%s, %d, ...)' % (pid, len(files)))
477 self.pid = pid
478 self.files = sorted(files, key=lambda x: x.path)
479 self.children = children
480 self.executable = executable
481 self.command = command
482 self.initial_cwd = initial_cwd
483
484 # Check internal consistency.
485 assert len(set(f.path for f in self.files)) == len(self.files), sorted(
486 f.path for f in self.files)
487 assert isinstance(self.children, list)
488 assert isinstance(self.files, list)
489
490 @property
491 def all(self):
492 for child in self.children:
493 for i in child.all:
494 yield i
495 yield self
496
497 def flatten(self):
498 return {
499 'children': [c.flatten() for c in self.children],
500 'command': self.command,
501 'executable': self.executable,
502 'files': [f.flatten() for f in self.files],
503 'initial_cwd': self.initial_cwd,
504 'pid': self.pid,
505 }
506
507 def strip_root(self, root):
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000508 assert file_path.isabs(root) and root.endswith(os.path.sep), root
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000509 # Loads the files after since they are constructed as objects.
510 out = self.__class__(
511 self.pid,
512 filter(None, (f.strip_root(root) for f in self.files)),
513 self.executable,
514 self.command,
515 self.initial_cwd,
516 [c.strip_root(root) for c in self.children])
517 logging.debug(
518 'strip_root(%s) %d -> %d' % (root, len(self.files), len(out.files)))
519 return out
520
521 def __init__(self, process):
522 self.process = process
523 # Cache.
524 self._files = None
525
526 def flatten(self):
527 return {
528 'root': self.process.flatten(),
529 }
530
531 @property
532 def files(self):
533 if self._files is None:
534 self._files = sorted(
535 sum((p.files for p in self.process.all), []),
536 key=lambda x: x.path)
537 return self._files
538
539 @property
540 def existent(self):
541 return [f for f in self.files if f.existent]
542
543 @property
544 def non_existent(self):
545 return [f for f in self.files if not f.existent]
546
547 def strip_root(self, root):
548 """Returns a clone with all the files outside the directory |root| removed
549 and converts all the path to be relative paths.
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000550
551 It keeps files accessible through the |root| directory or that have been
552 accessed through any symlink which points to the same directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000553 """
554 # Resolve any symlink
555 root = os.path.realpath(root)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000556 root = (
557 file_path.get_native_path_case(root).rstrip(os.path.sep) + os.path.sep)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000558 logging.debug('strip_root(%s)' % root)
559 return Results(self.process.strip_root(root))
560
561
562class ApiBase(object):
563 """OS-agnostic API to trace a process and its children."""
564 class Context(object):
565 """Processes one log line at a time and keeps the list of traced processes.
566
567 The parsing is complicated by the fact that logs are traced out of order for
568 strace but in-order for dtrace and logman. In addition, on Windows it is
569 very frequent that processids are reused so a flat list cannot be used. But
570 at the same time, it is impossible to faithfully construct a graph when the
571 logs are processed out of order. So both a tree and a flat mapping are used,
572 the tree is the real process tree, while the flat mapping stores the last
573 valid process for the corresponding processid. For the strace case, the
574 tree's head is guessed at the last moment.
575 """
576 class Process(object):
577 """Keeps context for one traced child process.
578
579 Logs all the files this process touched. Ignores directories.
580 """
581 def __init__(self, blacklist, pid, initial_cwd):
582 # Check internal consistency.
583 assert isinstance(pid, int), repr(pid)
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000584 assert_is_renderable(initial_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000585 self.pid = pid
586 # children are Process instances.
587 self.children = []
588 self.initial_cwd = initial_cwd
589 self.cwd = None
maruel@chromium.org538141b2013-06-03 20:57:17 +0000590 self.files = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000591 self.executable = None
592 self.command = None
593 self._blacklist = blacklist
594
595 def to_results_process(self):
596 """Resolves file case sensitivity and or late-bound strings."""
597 # When resolving files, it's normal to get dupe because a file could be
598 # opened multiple times with different case. Resolve the deduplication
599 # here.
600 def fix_path(x):
601 """Returns the native file path case.
602
603 Converts late-bound strings.
604 """
605 if not x:
606 # Do not convert None instance to 'None'.
607 return x
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000608 x = render(x)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000609 if os.path.isabs(x):
610 # If the path is not absolute, which tends to happen occasionally on
611 # Windows, it is not possible to get the native path case so ignore
612 # that trace. It mostly happens for 'executable' value.
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000613 x = file_path.get_native_path_case(x)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000614 return x
615
maruel@chromium.org538141b2013-06-03 20:57:17 +0000616 def fix_and_blacklist_path(x, m):
617 """Receives a tuple (filepath, mode) and processes filepath."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000618 x = fix_path(x)
619 if not x:
620 return
621 # The blacklist needs to be reapplied, since path casing could
622 # influence blacklisting.
623 if self._blacklist(x):
624 return
maruel@chromium.org538141b2013-06-03 20:57:17 +0000625 # Filters out directories. Some may have passed through.
626 if os.path.isdir(x):
627 return
628 return x, m
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000629
maruel@chromium.org538141b2013-06-03 20:57:17 +0000630 # Renders all the files as strings, as some could be RelativePath
631 # instances. It is important to do it first since there could still be
632 # multiple entries with the same path but different modes.
633 rendered = (
634 fix_and_blacklist_path(f, m) for f, m in self.files.iteritems())
635 files = sorted(
636 (f for f in rendered if f),
637 key=lambda x: (x[0], Results.File.ACCEPTABLE_MODES.index(x[1])))
638 # Then converting into a dict will automatically clean up lesser
639 # important values.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000640 files = [
maruel@chromium.org538141b2013-06-03 20:57:17 +0000641 Results.File(None, f, False, None, m)
642 for f, m in dict(files).iteritems()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000643 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000644 return Results.Process(
645 self.pid,
646 files,
647 fix_path(self.executable),
648 self.command,
649 fix_path(self.initial_cwd),
650 [c.to_results_process() for c in self.children])
651
maruel@chromium.org538141b2013-06-03 20:57:17 +0000652 def add_file(self, filepath, mode):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000653 """Adds a file if it passes the blacklist."""
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000654 if self._blacklist(render(filepath)):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000655 return
maruel@chromium.org538141b2013-06-03 20:57:17 +0000656 logging.debug('add_file(%d, %s, %s)', self.pid, filepath, mode)
657 # Note that filepath and not render(filepath) is added. It is because
658 # filepath could be something else than a string, like a RelativePath
659 # instance for dtrace logs.
660 modes = Results.File.ACCEPTABLE_MODES
661 old_mode = self.files.setdefault(filepath, mode)
662 if old_mode != mode and modes.index(old_mode) < modes.index(mode):
663 # Take the highest value.
664 self.files[filepath] = mode
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000665
666 def __init__(self, blacklist):
667 self.blacklist = blacklist
668 # Initial process.
669 self.root_process = None
670 # dict to accelerate process lookup, to not have to lookup the whole graph
671 # each time.
672 self._process_lookup = {}
673
674 class Tracer(object):
675 """During it's lifetime, the tracing subsystem is enabled."""
676 def __init__(self, logname):
677 self._logname = logname
maruel@chromium.org808e3f22013-05-16 16:42:54 +0000678 self._lock = threading.RLock()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000679 self._traces = []
680 self._initialized = True
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +0000681 self._scripts_to_cleanup = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000682
683 def trace(self, cmd, cwd, tracename, output):
684 """Runs the OS-specific trace program on an executable.
685
686 Arguments:
687 - cmd: The command (a list) to run.
688 - cwd: Current directory to start the child process in.
689 - tracename: Name of the trace in the logname file.
690 - output: If False, redirects output to PIPEs.
691
692 Returns a tuple (resultcode, output) and updates the internal trace
693 entries.
694 """
695 # The implementation adds an item to self._traces.
696 raise NotImplementedError(self.__class__.__name__)
697
698 def close(self, _timeout=None):
699 """Saves the meta-data in the logname file.
700
701 For kernel-based tracing, stops the tracing subsystem.
702
703 Must not be used manually when using 'with' construct.
704 """
705 with self._lock:
maruel@chromium.org808e3f22013-05-16 16:42:54 +0000706 if not self._initialized:
707 raise TracingFailure(
708 'Called %s.close() on an unitialized object' %
709 self.__class__.__name__,
710 None, None, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000711 try:
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +0000712 while self._scripts_to_cleanup:
713 try:
714 os.remove(self._scripts_to_cleanup.pop())
715 except OSError as e:
716 logging.error('Failed to delete a temporary script: %s', e)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500717 tools.write_json(self._logname, self._gen_logdata(), False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000718 finally:
719 self._initialized = False
720
721 def post_process_log(self):
722 """Post-processes the log so it becomes faster to load afterward.
723
724 Must not be used manually when using 'with' construct.
725 """
726 assert not self._initialized, 'Must stop tracing first.'
727
maruel@chromium.org808e3f22013-05-16 16:42:54 +0000728 def _gen_logdata(self):
729 """Returns the data to be saved in the trace file."""
730 return {
731 'traces': self._traces,
732 }
733
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000734 def __enter__(self):
735 """Enables 'with' statement."""
736 return self
737
738 def __exit__(self, exc_type, exc_value, traceback):
739 """Enables 'with' statement."""
740 self.close()
741 # If an exception was thrown, do not process logs.
742 if not exc_type:
743 self.post_process_log()
744
745 def get_tracer(self, logname):
746 """Returns an ApiBase.Tracer instance.
747
748 Initializes the tracing subsystem, which is a requirement for kernel-based
749 tracers. Only one tracer instance should be live at a time!
750
751 logname is the filepath to the json file that will contain the meta-data
752 about the logs.
753 """
754 return self.Tracer(logname)
755
756 @staticmethod
757 def clean_trace(logname):
758 """Deletes an old log."""
759 raise NotImplementedError()
760
761 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +0000762 def parse_log(cls, logname, blacklist, trace_name):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000763 """Processes trace logs and returns the files opened and the files that do
764 not exist.
765
766 It does not track directories.
767
maruel@chromium.orgec74ff82012-10-29 18:14:47 +0000768 Arguments:
769 - logname: must be an absolute path.
770 - blacklist: must be a lambda.
771 - trace_name: optional trace to read, defaults to reading all traces.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000772
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000773 Most of the time, files that do not exist are temporary test files that
774 should be put in /tmp instead. See http://crbug.com/116251.
775
776 Returns a list of dict with keys:
777 - results: A Results instance.
778 - trace: The corresponding tracename parameter provided to
779 get_tracer().trace().
780 - output: Output gathered during execution, if get_tracer().trace(...,
781 output=False) was used.
782 """
783 raise NotImplementedError(cls.__class__.__name__)
784
785
786class Strace(ApiBase):
787 """strace implies linux."""
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000788 @staticmethod
789 def load_filename(filename):
790 """Parses a filename in a log."""
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000791 # TODO(maruel): Be compatible with strace -x.
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000792 assert isinstance(filename, str)
793 out = ''
794 i = 0
795 while i < len(filename):
796 c = filename[i]
797 if c == '\\':
798 out += chr(int(filename[i+1:i+4], 8))
799 i += 4
800 else:
801 out += c
802 i += 1
803 # TODO(maruel): That's not necessarily true that the current code page is
804 # utf-8.
805 return out.decode('utf-8')
806
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000807 class Context(ApiBase.Context):
808 """Processes a strace log line and keeps the list of existent and non
809 existent files accessed.
810
811 Ignores directories.
812
813 Uses late-binding to processes the cwd of each process. The problem is that
814 strace generates one log file per process it traced but doesn't give any
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000815 information about which process was started when and by who. So process the
816 logs out of order and use late binding with RelativePath to be able to
817 deduce the initial directory of each process once all the logs are parsed.
818
819 TODO(maruel): Use the script even in the non-sudo case, so log parsing can
820 be done in two phase: first find the root process, then process the child
821 processes in order. With that, it should be possible to not use RelativePath
822 anymore. This would significantly simplify the code!
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000823 """
824 class Process(ApiBase.Context.Process):
825 """Represents the state of a process.
826
827 Contains all the information retrieved from the pid-specific log.
828 """
829 # Function names are using ([a-z_0-9]+)
830 # This is the most common format. function(args) = result
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000831 RE_HEADER = re.compile(r'^([a-z_0-9]+)\((.*?)\)\s+= (.+)$')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000832 # An interrupted function call, only grab the minimal header.
833 RE_UNFINISHED = re.compile(r'^([^\(]+)(.*) \<unfinished \.\.\.\>$')
834 # A resumed function call.
835 RE_RESUMED = re.compile(r'^<\.\.\. ([^ ]+) resumed> (.+)$')
836 # A process received a signal.
837 RE_SIGNAL = re.compile(r'^--- SIG[A-Z]+ .+ ---')
838 # A process didn't handle a signal. Ignore any junk appearing before,
839 # because the process was forcibly killed so it won't open any new file.
840 RE_KILLED = re.compile(
841 r'^.*\+\+\+ killed by ([A-Z]+)( \(core dumped\))? \+\+\+$')
842 # The process has exited.
843 RE_PROCESS_EXITED = re.compile(r'^\+\+\+ exited with (\d+) \+\+\+')
844 # A call was canceled. Ignore any prefix.
845 RE_UNAVAILABLE = re.compile(r'^.*\)\s*= \? <unavailable>$')
846 # Happens when strace fails to even get the function name.
847 UNNAMED_FUNCTION = '????'
848
849 # Corner-case in python, a class member function decorator must not be
850 # @staticmethod.
851 def parse_args(regexp, expect_zero): # pylint: disable=E0213
852 """Automatically convert the str 'args' into a list of processed
853 arguments.
854
855 Arguments:
856 - regexp is used to parse args.
857 - expect_zero: one of True, False or None.
858 - True: will check for result.startswith('0') first and will ignore
859 the trace line completely otherwise. This is important because for
860 many functions, the regexp will not process if the call failed.
861 - False: will check for not result.startswith(('?', '-1')) for the
862 same reason than with True.
863 - None: ignore result.
864 """
865 def meta_hook(function):
866 assert function.__name__.startswith('handle_')
867 def hook(self, args, result):
868 if expect_zero is True and not result.startswith('0'):
869 return
870 if expect_zero is False and result.startswith(('?', '-1')):
871 return
872 match = re.match(regexp, args)
873 if not match:
874 raise TracingFailure(
875 'Failed to parse %s(%s) = %s' %
876 (function.__name__[len('handle_'):], args, result),
877 None, None, None)
878 return function(self, match.groups(), result)
879 return hook
880 return meta_hook
881
882 class RelativePath(object):
883 """A late-bound relative path."""
884 def __init__(self, parent, value):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000885 assert_is_renderable(parent)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000886 self.parent = parent
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000887 assert (
888 value is None or
889 (isinstance(value, unicode) and not os.path.isabs(value)))
890 self.value = value
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000891 if self.value:
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000892 # TODO(maruel): On POSIX, '\\' is a valid character so remove this
893 # assert.
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000894 assert '\\' not in self.value, value
895 assert '\\' not in self.value, (repr(value), repr(self.value))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000896
897 def render(self):
898 """Returns the current directory this instance is representing.
899
900 This function is used to return the late-bound value.
901 """
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000902 assert self.parent is not None
903 parent = render(self.parent)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000904 if self.value:
905 return os.path.normpath(os.path.join(parent, self.value))
906 return parent
907
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000908 def __init__(self, root, pid):
909 """Keeps enough information to be able to guess the original process
910 root.
911
912 strace doesn't store which process was the initial process. So more
913 information needs to be kept so the graph can be reconstructed from the
914 flat map.
915 """
916 logging.info('%s(%d)' % (self.__class__.__name__, pid))
917 super(Strace.Context.Process, self).__init__(root.blacklist, pid, None)
918 assert isinstance(root, ApiBase.Context)
919 self._root = weakref.ref(root)
920 # The dict key is the function name of the pending call, like 'open'
921 # or 'execve'.
922 self._pending_calls = {}
923 self._line_number = 0
924 # Current directory when the process started.
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000925 if isinstance(self._root(), unicode):
926 self.initial_cwd = self._root()
927 else:
928 self.initial_cwd = self.RelativePath(self._root(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000929 self.parentid = None
maruel@chromium.org9f72e0f2012-10-18 14:48:49 +0000930 self._done = False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000931
932 def get_cwd(self):
933 """Returns the best known value of cwd."""
934 return self.cwd or self.initial_cwd
935
936 def render(self):
937 """Returns the string value of the RelativePath() object.
938
939 Used by RelativePath. Returns the initial directory and not the
940 current one since the current directory 'cwd' validity is time-limited.
941
942 The validity is only guaranteed once all the logs are processed.
943 """
944 return self.initial_cwd.render()
945
946 def on_line(self, line):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000947 assert isinstance(line, str)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000948 self._line_number += 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000949 try:
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000950 if self._done:
951 raise TracingFailure(
952 'Found a trace for a terminated process or corrupted log',
953 None, None, None)
954
955 if self.RE_SIGNAL.match(line):
956 # Ignore signals.
957 return
958
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000959 match = self.RE_KILLED.match(line)
960 if match:
961 # Converts a '+++ killed by Foo +++' trace into an exit_group().
962 self.handle_exit_group(match.group(1), None)
963 return
964
965 match = self.RE_PROCESS_EXITED.match(line)
966 if match:
967 # Converts a '+++ exited with 1 +++' trace into an exit_group()
968 self.handle_exit_group(match.group(1), None)
969 return
970
971 match = self.RE_UNFINISHED.match(line)
972 if match:
973 if match.group(1) in self._pending_calls:
974 raise TracingFailure(
975 'Found two unfinished calls for the same function',
976 None, None, None,
977 self._pending_calls)
978 self._pending_calls[match.group(1)] = (
979 match.group(1) + match.group(2))
980 return
981
982 match = self.RE_UNAVAILABLE.match(line)
983 if match:
984 # This usually means a process was killed and a pending call was
985 # canceled.
986 # TODO(maruel): Look up the last exit_group() trace just above and
987 # make sure any self._pending_calls[anything] is properly flushed.
988 return
989
990 match = self.RE_RESUMED.match(line)
991 if match:
992 if match.group(1) not in self._pending_calls:
993 raise TracingFailure(
994 'Found a resumed call that was not logged as unfinished',
995 None, None, None,
996 self._pending_calls)
997 pending = self._pending_calls.pop(match.group(1))
998 # Reconstruct the line.
999 line = pending + match.group(2)
1000
1001 match = self.RE_HEADER.match(line)
1002 if not match:
maruel@chromium.orgbcfc9e12013-03-19 18:47:17 +00001003 # The line is corrupted. It happens occasionally when a process is
1004 # killed forcibly with activity going on. Assume the process died.
1005 # No other line can be processed afterward.
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001006 logging.debug('%d is done: %s', self.pid, line)
maruel@chromium.orgbcfc9e12013-03-19 18:47:17 +00001007 self._done = True
1008 return
1009
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001010 if match.group(1) == self.UNNAMED_FUNCTION:
1011 return
1012
1013 # It's a valid line, handle it.
1014 handler = getattr(self, 'handle_%s' % match.group(1), None)
1015 if not handler:
1016 self._handle_unknown(match.group(1), match.group(2), match.group(3))
1017 return handler(match.group(2), match.group(3))
1018 except TracingFailure, e:
1019 # Hack in the values since the handler could be a static function.
1020 e.pid = self.pid
1021 e.line = line
1022 e.line_number = self._line_number
1023 # Re-raise the modified exception.
1024 raise
1025 except (KeyError, NotImplementedError, ValueError), e:
1026 raise TracingFailure(
1027 'Trace generated a %s exception: %s' % (
1028 e.__class__.__name__, str(e)),
1029 self.pid,
1030 self._line_number,
1031 line,
1032 e)
1033
1034 @parse_args(r'^\"(.+?)\", [FKORWX_|]+$', True)
1035 def handle_access(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001036 self._handle_file(args[0], Results.File.TOUCHED)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001037
1038 @parse_args(r'^\"(.+?)\"$', True)
1039 def handle_chdir(self, args, _result):
1040 """Updates cwd."""
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001041 self.cwd = self._mangle(args[0])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001042 logging.debug('handle_chdir(%d, %s)' % (self.pid, self.cwd))
1043
maruel@chromium.org538141b2013-06-03 20:57:17 +00001044 @parse_args(r'^\"(.+?)\", (\d+), (\d+)$', False)
1045 def handle_chown(self, args, _result):
1046 # TODO(maruel): Look at result?
1047 self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org55550722013-05-28 14:43:14 +00001048
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001049 def handle_clone(self, _args, result):
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001050 self._handling_forking('clone', result)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001051
1052 def handle_close(self, _args, _result):
1053 pass
1054
maruel@chromium.org538141b2013-06-03 20:57:17 +00001055 @parse_args(r'^\"(.+?)\", (\d+)$', False)
1056 def handle_chmod(self, args, _result):
1057 self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001058
maruel@chromium.org538141b2013-06-03 20:57:17 +00001059 @parse_args(r'^\"(.+?)\", (\d+)$', False)
1060 def handle_creat(self, args, _result):
1061 self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001062
1063 @parse_args(r'^\"(.+?)\", \[(.+)\], \[\/\* \d+ vars? \*\/\]$', True)
1064 def handle_execve(self, args, _result):
1065 # Even if in practice execve() doesn't returns when it succeeds, strace
1066 # still prints '0' as the result.
1067 filepath = args[0]
maruel@chromium.org538141b2013-06-03 20:57:17 +00001068 self._handle_file(filepath, Results.File.READ)
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001069 self.executable = self._mangle(filepath)
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001070 try:
1071 self.command = strace_process_quoted_arguments(args[1])
1072 except ValueError as e:
1073 raise TracingFailure(
1074 'Failed to process command line argument:\n%s' % e.args[0],
1075 None, None, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001076
1077 def handle_exit_group(self, _args, _result):
1078 """Removes cwd."""
1079 self.cwd = None
1080
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001081 @parse_args(r'^(\d+|AT_FDCWD), \"(.*?)\", ([A-Z\_\|]+)(|, \d+)$', True)
1082 def handle_faccessat(self, args, _results):
1083 if args[0] == 'AT_FDCWD':
maruel@chromium.org538141b2013-06-03 20:57:17 +00001084 self._handle_file(args[1], Results.File.TOUCHED)
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001085 else:
1086 raise Exception('Relative faccess not implemented.')
1087
maruel@chromium.org0781f322013-05-28 19:45:49 +00001088 def handle_fallocate(self, _args, result):
1089 pass
1090
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001091 def handle_fork(self, args, result):
1092 self._handle_unknown('fork', args, result)
1093
maruel@chromium.org9f72e0f2012-10-18 14:48:49 +00001094 def handle_futex(self, _args, _result):
1095 pass
1096
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001097 @parse_args(r'^(\".+?\"|0x[a-f0-9]+), (\d+)$', False)
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001098 def handle_getcwd(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001099 # TODO(maruel): Resolve file handle.
1100 if not args[0].startswith('0x'):
1101 filepath = args[0][1:-1]
1102 if os.path.isabs(filepath):
1103 logging.debug('handle_chdir(%d, %s)' % (self.pid, self.cwd))
1104 if not isinstance(self.cwd, unicode):
1105 # Take the occasion to reset the path.
1106 self.cwd = self._mangle(filepath)
1107 else:
1108 # It should always match.
1109 assert self.cwd == Strace.load_filename(filepath), (
1110 self.cwd, filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001111
1112 @parse_args(r'^\"(.+?)\", \"(.+?)\"$', True)
1113 def handle_link(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001114 self._handle_file(args[0], Results.File.READ)
1115 self._handle_file(args[1], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001116
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001117 @parse_args(r'^(\".+?\"|0x[a-f0-9]+), \{(?:.+?, )?\.\.\.\}$', True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001118 def handle_lstat(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001119 # TODO(maruel): Resolve file handle.
1120 if not args[0].startswith('0x'):
1121 self._handle_file(args[0][1:-1], Results.File.TOUCHED)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001122
1123 def handle_mkdir(self, _args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001124 # We track content, not directories.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001125 pass
1126
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001127 @parse_args(r'^(\".*?\"|0x[a-f0-9]+), ([A-Z\_\|]+)(|, \d+)$', False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001128 def handle_open(self, args, _result):
1129 if 'O_DIRECTORY' in args[1]:
1130 return
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001131 # TODO(maruel): Resolve file handle.
1132 if not args[0].startswith('0x'):
1133 t = Results.File.READ if 'O_RDONLY' in args[1] else Results.File.WRITE
1134 self._handle_file(args[0][1:-1], t)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001135
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001136 @parse_args(
1137 r'^(\d+|AT_FDCWD), (\".*?\"|0x[a-f0-9]+), ([A-Z\_\|]+)(|, \d+)$',
1138 False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001139 def handle_openat(self, args, _result):
1140 if 'O_DIRECTORY' in args[2]:
1141 return
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001142 if args[0] == 'AT_FDCWD':
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001143 # TODO(maruel): Resolve file handle.
1144 if not args[1].startswith('0x'):
1145 t = (
1146 Results.File.READ if 'O_RDONLY' in args[2]
maruel@chromium.org538141b2013-06-03 20:57:17 +00001147 else Results.File.WRITE)
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001148 self._handle_file(args[1][1:-1], t)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001149 else:
1150 # TODO(maruel): Implement relative open if necessary instead of the
1151 # AT_FDCWD flag, let's hope not since this means tracking all active
1152 # directory handles.
maruel@chromium.org538141b2013-06-03 20:57:17 +00001153 raise NotImplementedError('Relative open via openat not implemented.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001154
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001155 @parse_args(
1156 r'^(\".+?\"|0x[a-f0-9]+), (?:\".+?\"(?:\.\.\.)?|0x[a-f0-9]+), '
1157 '\d+$',
1158 False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001159 def handle_readlink(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001160 # TODO(maruel): Resolve file handle.
1161 if not args[0].startswith('0x'):
1162 self._handle_file(args[0][1:-1], Results.File.READ)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001163
1164 @parse_args(r'^\"(.+?)\", \"(.+?)\"$', True)
1165 def handle_rename(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001166 self._handle_file(args[0], Results.File.READ)
1167 self._handle_file(args[1], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001168
1169 def handle_rmdir(self, _args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001170 # We track content, not directories.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001171 pass
1172
1173 def handle_setxattr(self, _args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001174 # TODO(maruel): self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001175 pass
1176
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001177 @parse_args(r'^(\".+?\"|0x[a-f0-9]+), \{.+?\}$', True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001178 def handle_stat(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001179 # TODO(maruel): Resolve file handle.
1180 if not args[0].startswith('0x'):
1181 self._handle_file(args[0][1:-1], Results.File.TOUCHED)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001182
maruel@chromium.org538141b2013-06-03 20:57:17 +00001183 @parse_args(r'^\"(.+?)\", \"(.+?)\"$', True)
1184 def handle_symlink(self, args, _result):
1185 self._handle_file(args[0], Results.File.TOUCHED)
1186 self._handle_file(args[1], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001187
csharp@chromium.orgfa672922012-11-02 17:43:31 +00001188 @parse_args(r'^\"(.+?)\", \d+', True)
1189 def handle_truncate(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001190 self._handle_file(args[0], Results.File.WRITE)
csharp@chromium.orgfa672922012-11-02 17:43:31 +00001191
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001192 def handle_unlink(self, _args, _result):
1193 # In theory, the file had to be created anyway.
1194 pass
1195
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001196 def handle_unlinkat(self, _args, _result):
1197 # In theory, the file had to be created anyway.
1198 pass
1199
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001200 def handle_statfs(self, _args, _result):
1201 pass
1202
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001203 def handle_utimensat(self, _args, _result):
1204 pass
1205
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001206 def handle_vfork(self, _args, result):
1207 self._handling_forking('vfork', result)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001208
1209 @staticmethod
1210 def _handle_unknown(function, args, result):
1211 raise TracingFailure(
1212 'Unexpected/unimplemented trace %s(%s)= %s' %
1213 (function, args, result),
1214 None, None, None)
1215
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001216 def _handling_forking(self, name, result):
1217 """Transfers cwd."""
1218 if result.startswith(('?', '-1')):
1219 # The call failed.
1220 return
1221 # Update the other process right away.
1222 childpid = int(result)
1223 child = self._root().get_or_set_proc(childpid)
1224 if child.parentid is not None or childpid in self.children:
1225 raise TracingFailure(
1226 'Found internal inconsitency in process lifetime detection '
1227 'during a %s() call' % name,
1228 None, None, None)
1229
1230 # Copy the cwd object.
1231 child.initial_cwd = self.get_cwd()
1232 child.parentid = self.pid
1233 # It is necessary because the logs are processed out of order.
1234 self.children.append(child)
1235
maruel@chromium.org538141b2013-06-03 20:57:17 +00001236 def _handle_file(self, filepath, mode):
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001237 filepath = self._mangle(filepath)
maruel@chromium.org538141b2013-06-03 20:57:17 +00001238 self.add_file(filepath, mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001239
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001240 def _mangle(self, filepath):
1241 """Decodes a filepath found in the log and convert it to a late-bound
1242 path if necessary.
1243
1244 |filepath| is an strace 'encoded' string and the returned value is
1245 either an unicode string if the path was absolute or a late bound path
1246 otherwise.
1247 """
1248 filepath = Strace.load_filename(filepath)
1249 if os.path.isabs(filepath):
1250 return filepath
1251 else:
1252 if isinstance(self.get_cwd(), unicode):
1253 return os.path.normpath(os.path.join(self.get_cwd(), filepath))
1254 return self.RelativePath(self.get_cwd(), filepath)
1255
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001256 def __init__(self, blacklist, root_pid, initial_cwd):
1257 """|root_pid| may be None when the root process is not known.
1258
1259 In that case, a search is done after reading all the logs to figure out
1260 the root process.
1261 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001262 super(Strace.Context, self).__init__(blacklist)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001263 assert_is_renderable(initial_cwd)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001264 self.root_pid = root_pid
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001265 self.initial_cwd = initial_cwd
1266
1267 def render(self):
1268 """Returns the string value of the initial cwd of the root process.
1269
1270 Used by RelativePath.
1271 """
1272 return self.initial_cwd
1273
1274 def on_line(self, pid, line):
1275 """Transfers control into the Process.on_line() function."""
1276 self.get_or_set_proc(pid).on_line(line.strip())
1277
1278 def to_results(self):
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001279 """If necessary, finds back the root process and verify consistency."""
1280 if not self.root_pid:
1281 # The non-sudo case. The traced process was started by strace itself,
1282 # so the pid of the traced process is not known.
1283 root = [p for p in self._process_lookup.itervalues() if not p.parentid]
1284 if len(root) == 1:
1285 self.root_process = root[0]
1286 # Save it for later.
1287 self.root_pid = self.root_process.pid
1288 else:
1289 # The sudo case. The traced process was started manually so its pid is
1290 # known.
1291 self.root_process = self._process_lookup.get(self.root_pid)
1292 if not self.root_process:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001293 raise TracingFailure(
1294 'Found internal inconsitency in process lifetime detection '
1295 'while finding the root process',
1296 None,
1297 None,
1298 None,
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001299 self.root_pid,
1300 sorted(self._process_lookup))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001301 process = self.root_process.to_results_process()
1302 if sorted(self._process_lookup) != sorted(p.pid for p in process.all):
1303 raise TracingFailure(
1304 'Found internal inconsitency in process lifetime detection '
1305 'while looking for len(tree) == len(list)',
1306 None,
1307 None,
1308 None,
1309 sorted(self._process_lookup),
1310 sorted(p.pid for p in process.all))
1311 return Results(process)
1312
1313 def get_or_set_proc(self, pid):
1314 """Returns the Context.Process instance for this pid or creates a new one.
1315 """
1316 if not pid or not isinstance(pid, int):
1317 raise TracingFailure(
1318 'Unpexpected value for pid: %r' % pid,
1319 pid,
1320 None,
1321 None,
1322 pid)
1323 if pid not in self._process_lookup:
1324 self._process_lookup[pid] = self.Process(self, pid)
1325 return self._process_lookup[pid]
1326
1327 @classmethod
1328 def traces(cls):
1329 """Returns the list of all handled traces to pass this as an argument to
1330 strace.
1331 """
1332 prefix = 'handle_'
1333 return [i[len(prefix):] for i in dir(cls.Process) if i.startswith(prefix)]
1334
1335 class Tracer(ApiBase.Tracer):
1336 MAX_LEN = 256
1337
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00001338 def __init__(self, logname, use_sudo):
1339 super(Strace.Tracer, self).__init__(logname)
1340 self.use_sudo = use_sudo
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001341 if use_sudo:
1342 # TODO(maruel): Use the jump script systematically to make it easy to
1343 # figure out the root process, so RelativePath is not necessary anymore.
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00001344 self._child_script = create_exec_thunk()
1345 self._scripts_to_cleanup.append(self._child_script)
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00001346
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001347 def trace(self, cmd, cwd, tracename, output):
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001348 """Runs strace on an executable.
1349
1350 When use_sudo=True, it is a 3-phases process: start the thunk, start
1351 sudo strace with the pid of the thunk and then have the thunk os.execve()
1352 the process to trace.
1353 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001354 logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
1355 assert os.path.isabs(cmd[0]), cmd[0]
1356 assert os.path.isabs(cwd), cwd
1357 assert os.path.normpath(cwd) == cwd, cwd
1358 with self._lock:
1359 if not self._initialized:
1360 raise TracingFailure(
1361 'Called Tracer.trace() on an unitialized object',
1362 None, None, None, tracename)
1363 assert tracename not in (i['trace'] for i in self._traces)
1364 stdout = stderr = None
1365 if output:
1366 stdout = subprocess.PIPE
1367 stderr = subprocess.STDOUT
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001368
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001369 # Ensure all file related APIs are hooked.
1370 traces = ','.join(Strace.Context.traces() + ['file'])
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001371 flags = [
maruel@chromium.orga5cfdc32013-05-28 14:46:15 +00001372 # Each child process has its own trace file. It is necessary because
1373 # strace may generate corrupted log file if multiple processes are
1374 # heavily doing syscalls simultaneously.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001375 '-ff',
maruel@chromium.orga5cfdc32013-05-28 14:46:15 +00001376 # Reduce whitespace usage.
1377 '-a1',
1378 # hex encode non-ascii strings.
1379 # TODO(maruel): '-x',
1380 # TODO(maruel): '-ttt',
1381 # Signals are unnecessary noise here. Note the parser can cope with them
1382 # but reduce the unnecessary output.
1383 '-esignal=none',
1384 # Print as much data as wanted.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001385 '-s', '%d' % self.MAX_LEN,
1386 '-e', 'trace=%s' % traces,
1387 '-o', self._logname + '.' + tracename,
1388 ]
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001389
1390 if self.use_sudo:
1391 pipe_r, pipe_w = os.pipe()
1392 # Start the child process paused.
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00001393 target_cmd = [sys.executable, self._child_script, str(pipe_r)] + cmd
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001394 logging.debug(' '.join(target_cmd))
1395 child_proc = subprocess.Popen(
1396 target_cmd,
1397 stdin=subprocess.PIPE,
1398 stdout=stdout,
1399 stderr=stderr,
1400 cwd=cwd)
1401
1402 # TODO(maruel): both processes must use the same UID for it to work
1403 # without sudo. Look why -p is failing at the moment without sudo.
1404 trace_cmd = [
1405 'sudo',
1406 'strace',
1407 '-p', str(child_proc.pid),
1408 ] + flags
1409 logging.debug(' '.join(trace_cmd))
1410 strace_proc = subprocess.Popen(
1411 trace_cmd,
1412 cwd=cwd,
1413 stdin=subprocess.PIPE,
1414 stdout=subprocess.PIPE,
1415 stderr=subprocess.PIPE)
1416
1417 line = strace_proc.stderr.readline()
1418 if not re.match(r'^Process \d+ attached \- interrupt to quit$', line):
1419 # TODO(maruel): Raise an exception.
1420 assert False, line
1421
1422 # Now fire the child process.
1423 os.write(pipe_w, 'x')
1424
1425 out = child_proc.communicate()[0]
1426 strace_out = strace_proc.communicate()[0]
1427
1428 # TODO(maruel): if strace_proc.returncode: Add an exception.
1429 saved_out = strace_out if strace_proc.returncode else out
1430 root_pid = child_proc.pid
1431 else:
1432 # Non-sudo case.
1433 trace_cmd = [
1434 'strace',
1435 ] + flags + cmd
1436 logging.debug(' '.join(trace_cmd))
1437 child_proc = subprocess.Popen(
1438 trace_cmd,
1439 cwd=cwd,
1440 stdin=subprocess.PIPE,
1441 stdout=stdout,
1442 stderr=stderr)
1443 out = child_proc.communicate()[0]
1444 # TODO(maruel): Walk the logs and figure out the root process would
1445 # simplify parsing the logs a *lot*.
1446 saved_out = out
1447 # The trace reader will have to figure out.
1448 root_pid = None
1449
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001450 with self._lock:
1451 assert tracename not in (i['trace'] for i in self._traces)
1452 self._traces.append(
1453 {
1454 'cmd': cmd,
1455 'cwd': cwd,
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001456 'output': saved_out,
1457 'pid': root_pid,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001458 'trace': tracename,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001459 })
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001460 return child_proc.returncode, out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001461
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00001462 def __init__(self, use_sudo=None):
1463 super(Strace, self).__init__()
1464 self.use_sudo = use_sudo
1465
1466 def get_tracer(self, logname):
1467 return self.Tracer(logname, self.use_sudo)
1468
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001469 @staticmethod
1470 def clean_trace(logname):
1471 if os.path.isfile(logname):
1472 os.remove(logname)
1473 # Also delete any pid specific file from previous traces.
1474 for i in glob.iglob(logname + '.*'):
1475 if i.rsplit('.', 1)[1].isdigit():
1476 os.remove(i)
1477
1478 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001479 def parse_log(cls, logname, blacklist, trace_name):
1480 logging.info('parse_log(%s, ..., %s)', logname, trace_name)
maruel@chromium.org8b056ba2012-10-16 14:04:49 +00001481 assert os.path.isabs(logname)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001482 data = tools.read_json(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001483 out = []
1484 for item in data['traces']:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001485 if trace_name and item['trace'] != trace_name:
1486 continue
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001487 result = {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 'output': item['output'],
maruel@chromium.org964ddde2012-10-29 18:13:46 +00001489 'trace': item['trace'],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001490 }
1491 try:
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001492 context = cls.Context(blacklist, item['pid'], item['cwd'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001493 for pidfile in glob.iglob('%s.%s.*' % (logname, item['trace'])):
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001494 logging.debug('Reading %s', pidfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001495 pid = pidfile.rsplit('.', 1)[1]
1496 if pid.isdigit():
1497 pid = int(pid)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001498 found_line = False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001499 for line in open(pidfile, 'rb'):
1500 context.on_line(pid, line)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001501 found_line = True
1502 if not found_line:
1503 # Ensures that a completely empty trace still creates the
1504 # corresponding Process instance by logging a dummy line.
1505 context.on_line(pid, '')
1506 else:
1507 logging.warning('Found unexpected file %s', pidfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001508 result['results'] = context.to_results()
maruel@chromium.org424f5692012-10-15 15:42:38 +00001509 except TracingFailure:
1510 result['exception'] = sys.exc_info()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001511 out.append(result)
1512 return out
1513
1514
1515class Dtrace(ApiBase):
1516 """Uses DTrace framework through dtrace. Requires root access.
1517
1518 Implies Mac OSX.
1519
1520 dtruss can't be used because it has compatibility issues with python.
1521
1522 Also, the pid->cwd handling needs to be done manually since OSX has no way to
1523 get the absolute path of the 'cwd' dtrace variable from the probe.
1524
1525 Also, OSX doesn't populate curpsinfo->pr_psargs properly, see
1526 https://discussions.apple.com/thread/1980539. So resort to handling execve()
1527 manually.
1528
1529 errno is not printed in the log since this implementation currently only cares
1530 about files that were successfully opened.
1531 """
1532 class Context(ApiBase.Context):
1533 # Format: index pid function(args)
1534 RE_HEADER = re.compile(r'^\d+ (\d+) ([a-zA-Z_\-]+)\((.*?)\)$')
1535
1536 # Arguments parsing.
1537 RE_DTRACE_BEGIN = re.compile(r'^\"(.+?)\"$')
1538 RE_CHDIR = re.compile(r'^\"(.+?)\"$')
1539 RE_EXECVE = re.compile(r'^\"(.+?)\", \[(\d+), (.+)\]$')
1540 RE_OPEN = re.compile(r'^\"(.+?)\", (0x[0-9a-z]+), (0x[0-9a-z]+)$')
1541 RE_PROC_START = re.compile(r'^(\d+), \"(.+?)\", (\d+)$')
1542 RE_RENAME = re.compile(r'^\"(.+?)\", \"(.+?)\"$')
1543
maruel@chromium.orgac361162013-06-04 15:54:06 +00001544 # O_DIRECTORY is not defined on Windows and dtrace doesn't exist on Windows.
1545 O_DIRECTORY = os.O_DIRECTORY if hasattr(os, 'O_DIRECTORY') else None
maruel@chromium.org538141b2013-06-03 20:57:17 +00001546 O_RDWR = os.O_RDWR
1547 O_WRONLY = os.O_WRONLY
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001548
1549 class Process(ApiBase.Context.Process):
1550 def __init__(self, *args):
1551 super(Dtrace.Context.Process, self).__init__(*args)
1552 self.cwd = self.initial_cwd
1553
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001554 def __init__(self, blacklist, thunk_pid, initial_cwd):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001555 logging.info(
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001556 '%s(%d, %s)' % (self.__class__.__name__, thunk_pid, initial_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001557 super(Dtrace.Context, self).__init__(blacklist)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001558 assert isinstance(initial_cwd, unicode), initial_cwd
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001559 # Process ID of the temporary script created by create_subprocess_thunk().
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001560 self._thunk_pid = thunk_pid
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001561 self._initial_cwd = initial_cwd
1562 self._line_number = 0
1563
1564 def on_line(self, line):
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001565 assert isinstance(line, unicode), line
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001566 self._line_number += 1
1567 match = self.RE_HEADER.match(line)
1568 if not match:
1569 raise TracingFailure(
1570 'Found malformed line: %s' % line,
1571 None,
1572 self._line_number,
1573 line)
1574 fn = getattr(
1575 self,
1576 'handle_%s' % match.group(2).replace('-', '_'),
1577 self._handle_ignored)
1578 # It is guaranteed to succeed because of the regexp. Or at least I thought
1579 # it would.
1580 pid = int(match.group(1))
1581 try:
1582 return fn(pid, match.group(3))
1583 except TracingFailure, e:
1584 # Hack in the values since the handler could be a static function.
1585 e.pid = pid
1586 e.line = line
1587 e.line_number = self._line_number
1588 # Re-raise the modified exception.
1589 raise
1590 except (KeyError, NotImplementedError, ValueError), e:
1591 raise TracingFailure(
1592 'Trace generated a %s exception: %s' % (
1593 e.__class__.__name__, str(e)),
1594 pid,
1595 self._line_number,
1596 line,
1597 e)
1598
1599 def to_results(self):
1600 process = self.root_process.to_results_process()
1601 # Internal concistency check.
1602 if sorted(self._process_lookup) != sorted(p.pid for p in process.all):
1603 raise TracingFailure(
1604 'Found internal inconsitency in process lifetime detection '
1605 'while looking for len(tree) == len(list)',
1606 None,
1607 None,
1608 None,
1609 sorted(self._process_lookup),
1610 sorted(p.pid for p in process.all))
1611 return Results(process)
1612
1613 def handle_dtrace_BEGIN(self, _pid, args):
1614 if not self.RE_DTRACE_BEGIN.match(args):
1615 raise TracingFailure(
1616 'Found internal inconsitency in dtrace_BEGIN log line',
1617 None, None, None)
1618
1619 def handle_proc_start(self, pid, args):
1620 """Transfers cwd.
1621
1622 The dtrace script already takes care of only tracing the processes that
1623 are child of the traced processes so there is no need to verify the
1624 process hierarchy.
1625 """
1626 if pid in self._process_lookup:
1627 raise TracingFailure(
1628 'Found internal inconsitency in proc_start: %d started two times' %
1629 pid,
1630 None, None, None)
1631 match = self.RE_PROC_START.match(args)
1632 if not match:
1633 raise TracingFailure(
1634 'Failed to parse arguments: %s' % args,
1635 None, None, None)
1636 ppid = int(match.group(1))
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001637 if ppid == self._thunk_pid and not self.root_process:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001638 proc = self.root_process = self.Process(
1639 self.blacklist, pid, self._initial_cwd)
1640 elif ppid in self._process_lookup:
1641 proc = self.Process(self.blacklist, pid, self._process_lookup[ppid].cwd)
1642 self._process_lookup[ppid].children.append(proc)
1643 else:
1644 # Another process tree, ignore.
1645 return
1646 self._process_lookup[pid] = proc
1647 logging.debug(
1648 'New child: %s -> %d cwd:%s' %
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001649 (ppid, pid, render(proc.initial_cwd)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001650
1651 def handle_proc_exit(self, pid, _args):
1652 """Removes cwd."""
1653 if pid in self._process_lookup:
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001654 # self._thunk_pid is not traced itself and other traces run neither.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001655 self._process_lookup[pid].cwd = None
1656
1657 def handle_execve(self, pid, args):
1658 """Sets the process' executable.
1659
1660 TODO(maruel): Read command line arguments. See
1661 https://discussions.apple.com/thread/1980539 for an example.
1662 https://gist.github.com/1242279
1663
1664 Will have to put the answer at http://stackoverflow.com/questions/7556249.
1665 :)
1666 """
1667 if not pid in self._process_lookup:
1668 # Another process tree, ignore.
1669 return
1670 match = self.RE_EXECVE.match(args)
1671 if not match:
1672 raise TracingFailure(
1673 'Failed to parse arguments: %r' % args,
1674 None, None, None)
1675 proc = self._process_lookup[pid]
1676 proc.executable = match.group(1)
maruel@chromium.org538141b2013-06-03 20:57:17 +00001677 self._handle_file(pid, proc.executable, Results.File.READ)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001678 proc.command = self.process_escaped_arguments(match.group(3))
1679 if int(match.group(2)) != len(proc.command):
1680 raise TracingFailure(
1681 'Failed to parse execve() arguments: %s' % args,
1682 None, None, None)
1683
1684 def handle_chdir(self, pid, args):
1685 """Updates cwd."""
1686 if pid not in self._process_lookup:
1687 # Another process tree, ignore.
1688 return
1689 cwd = self.RE_CHDIR.match(args).group(1)
1690 if not cwd.startswith('/'):
1691 cwd2 = os.path.join(self._process_lookup[pid].cwd, cwd)
1692 logging.debug('handle_chdir(%d, %s) -> %s' % (pid, cwd, cwd2))
1693 else:
1694 logging.debug('handle_chdir(%d, %s)' % (pid, cwd))
1695 cwd2 = cwd
1696 self._process_lookup[pid].cwd = cwd2
1697
1698 def handle_open_nocancel(self, pid, args):
1699 """Redirects to handle_open()."""
1700 return self.handle_open(pid, args)
1701
1702 def handle_open(self, pid, args):
1703 if pid not in self._process_lookup:
1704 # Another process tree, ignore.
1705 return
1706 match = self.RE_OPEN.match(args)
1707 if not match:
1708 raise TracingFailure(
1709 'Failed to parse arguments: %s' % args,
1710 None, None, None)
1711 flag = int(match.group(2), 16)
1712 if self.O_DIRECTORY & flag == self.O_DIRECTORY:
1713 # Ignore directories.
1714 return
maruel@chromium.org538141b2013-06-03 20:57:17 +00001715 self._handle_file(
1716 pid,
1717 match.group(1),
1718 Results.File.READ if not ((self.O_RDWR | self.O_WRONLY) & flag)
1719 else Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001720
1721 def handle_rename(self, pid, args):
1722 if pid not in self._process_lookup:
1723 # Another process tree, ignore.
1724 return
1725 match = self.RE_RENAME.match(args)
1726 if not match:
1727 raise TracingFailure(
1728 'Failed to parse arguments: %s' % args,
1729 None, None, None)
maruel@chromium.org538141b2013-06-03 20:57:17 +00001730 self._handle_file(pid, match.group(1), Results.File.READ)
1731 self._handle_file(pid, match.group(2), Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001732
maruel@chromium.org538141b2013-06-03 20:57:17 +00001733 def _handle_file(self, pid, filepath, mode):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001734 if not filepath.startswith('/'):
1735 filepath = os.path.join(self._process_lookup[pid].cwd, filepath)
1736 # We can get '..' in the path.
1737 filepath = os.path.normpath(filepath)
1738 # Sadly, still need to filter out directories here;
1739 # saw open_nocancel(".", 0, 0) = 0 lines.
1740 if os.path.isdir(filepath):
1741 return
maruel@chromium.org538141b2013-06-03 20:57:17 +00001742 self._process_lookup[pid].add_file(filepath, mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001743
1744 def handle_ftruncate(self, pid, args):
1745 """Just used as a signal to kill dtrace, ignoring."""
1746 pass
1747
1748 @staticmethod
1749 def _handle_ignored(pid, args):
1750 """Is called for all the event traces that are not handled."""
1751 raise NotImplementedError('Please implement me')
1752
1753 @staticmethod
1754 def process_escaped_arguments(text):
1755 """Extracts escaped arguments on a string and return the arguments as a
1756 list.
1757
1758 Implemented as an automaton.
1759
1760 Example:
1761 With text = '\\001python2.7\\001-c\\001print(\\"hi\\")\\0', the
1762 function will return ['python2.7', '-c', 'print("hi")]
1763 """
1764 if not text.endswith('\\0'):
1765 raise ValueError('String is not null terminated: %r' % text, text)
1766 text = text[:-2]
1767
1768 def unescape(x):
1769 """Replaces '\\' with '\' and '\?' (where ? is anything) with ?."""
1770 out = []
1771 escaped = False
1772 for i in x:
1773 if i == '\\' and not escaped:
1774 escaped = True
1775 continue
1776 escaped = False
1777 out.append(i)
1778 return ''.join(out)
1779
1780 return [unescape(i) for i in text.split('\\001')]
1781
1782 class Tracer(ApiBase.Tracer):
1783 # pylint: disable=C0301
1784 #
1785 # To understand the following code, you'll want to take a look at:
1786 # http://developers.sun.com/solaris/articles/dtrace_quickref/dtrace_quickref.html
1787 # https://wikis.oracle.com/display/DTrace/Variables
1788 # http://docs.oracle.com/cd/E19205-01/820-4221/
1789 #
1790 # 0. Dump all the valid probes into a text file. It is important, you
1791 # want to redirect into a file and you don't want to constantly 'sudo'.
1792 # $ sudo dtrace -l > probes.txt
1793 #
1794 # 1. Count the number of probes:
1795 # $ wc -l probes.txt
1796 # 81823 # On OSX 10.7, including 1 header line.
1797 #
1798 # 2. List providers, intentionally skipping all the 'syspolicy10925' and the
1799 # likes and skipping the header with NR>1:
1800 # $ awk 'NR>1 { print $2 }' probes.txt | sort | uniq | grep -v '[[:digit:]]'
1801 # dtrace
1802 # fbt
1803 # io
1804 # ip
1805 # lockstat
1806 # mach_trap
1807 # proc
1808 # profile
1809 # sched
1810 # syscall
1811 # tcp
1812 # vminfo
1813 #
1814 # 3. List of valid probes:
1815 # $ grep syscall probes.txt | less
1816 # or use dtrace directly:
1817 # $ sudo dtrace -l -P syscall | less
1818 #
1819 # trackedpid is an associative array where its value can be 0, 1 or 2.
1820 # 0 is for untracked processes and is the default value for items not
1821 # in the associative array.
1822 # 1 is for tracked processes.
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001823 # 2 is for the script created by create_subprocess_thunk() only. It is not
1824 # tracked itself but all its decendants are.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001825 #
1826 # The script will kill itself only once waiting_to_die == 1 and
1827 # current_processes == 0, so that both getlogin() was called and that
1828 # all traced processes exited.
1829 #
1830 # TODO(maruel): Use cacheable predicates. See
1831 # https://wikis.oracle.com/display/DTrace/Performance+Considerations
1832 D_CODE = """
1833 dtrace:::BEGIN {
1834 waiting_to_die = 0;
1835 current_processes = 0;
1836 logindex = 0;
1837 printf("%d %d %s_%s(\\"%s\\")\\n",
1838 logindex, PID, probeprov, probename, SCRIPT);
1839 logindex++;
1840 }
1841
1842 proc:::start /trackedpid[ppid]/ {
1843 trackedpid[pid] = 1;
1844 current_processes += 1;
1845 printf("%d %d %s_%s(%d, \\"%s\\", %d)\\n",
1846 logindex, pid, probeprov, probename,
1847 ppid,
1848 execname,
1849 current_processes);
1850 logindex++;
1851 }
1852 /* Should use SCRIPT but there is no access to this variable at that
1853 * point. */
1854 proc:::start /ppid == PID && execname == "Python"/ {
1855 trackedpid[pid] = 2;
1856 current_processes += 1;
1857 printf("%d %d %s_%s(%d, \\"%s\\", %d)\\n",
1858 logindex, pid, probeprov, probename,
1859 ppid,
1860 execname,
1861 current_processes);
1862 logindex++;
1863 }
1864 proc:::exit /trackedpid[pid] &&
1865 current_processes == 1 &&
1866 waiting_to_die == 1/ {
1867 trackedpid[pid] = 0;
1868 current_processes -= 1;
1869 printf("%d %d %s_%s(%d)\\n",
1870 logindex, pid, probeprov, probename,
1871 current_processes);
1872 logindex++;
1873 exit(0);
1874 }
1875 proc:::exit /trackedpid[pid]/ {
1876 trackedpid[pid] = 0;
1877 current_processes -= 1;
1878 printf("%d %d %s_%s(%d)\\n",
1879 logindex, pid, probeprov, probename,
1880 current_processes);
1881 logindex++;
1882 }
1883
1884 /* Use an arcane function to detect when we need to die */
1885 syscall::ftruncate:entry /pid == PID && arg0 == FILE_ID/ {
1886 waiting_to_die = 1;
1887 printf("%d %d %s()\\n", logindex, pid, probefunc);
1888 logindex++;
1889 }
1890 syscall::ftruncate:entry /
1891 pid == PID && arg0 == FILE_ID && current_processes == 0/ {
1892 exit(0);
1893 }
1894
1895 syscall::open*:entry /trackedpid[pid] == 1/ {
1896 self->open_arg0 = arg0;
1897 self->open_arg1 = arg1;
1898 self->open_arg2 = arg2;
1899 }
1900 syscall::open*:return /trackedpid[pid] == 1 && errno == 0/ {
1901 this->open_arg0 = copyinstr(self->open_arg0);
1902 printf("%d %d %s(\\"%s\\", 0x%x, 0x%x)\\n",
1903 logindex, pid, probefunc,
1904 this->open_arg0,
1905 self->open_arg1,
1906 self->open_arg2);
1907 logindex++;
1908 this->open_arg0 = 0;
1909 }
1910 syscall::open*:return /trackedpid[pid] == 1/ {
1911 self->open_arg0 = 0;
1912 self->open_arg1 = 0;
1913 self->open_arg2 = 0;
1914 }
1915
1916 syscall::rename:entry /trackedpid[pid] == 1/ {
1917 self->rename_arg0 = arg0;
1918 self->rename_arg1 = arg1;
1919 }
1920 syscall::rename:return /trackedpid[pid] == 1 && errno == 0/ {
1921 this->rename_arg0 = copyinstr(self->rename_arg0);
1922 this->rename_arg1 = copyinstr(self->rename_arg1);
1923 printf("%d %d %s(\\"%s\\", \\"%s\\")\\n",
1924 logindex, pid, probefunc,
1925 this->rename_arg0,
1926 this->rename_arg1);
1927 logindex++;
1928 this->rename_arg0 = 0;
1929 this->rename_arg1 = 0;
1930 }
1931 syscall::rename:return /trackedpid[pid] == 1/ {
1932 self->rename_arg0 = 0;
1933 self->rename_arg1 = 0;
1934 }
1935
1936 /* Track chdir, it's painful because it is only receiving relative path.
1937 */
1938 syscall::chdir:entry /trackedpid[pid] == 1/ {
1939 self->chdir_arg0 = arg0;
1940 }
1941 syscall::chdir:return /trackedpid[pid] == 1 && errno == 0/ {
1942 this->chdir_arg0 = copyinstr(self->chdir_arg0);
1943 printf("%d %d %s(\\"%s\\")\\n",
1944 logindex, pid, probefunc,
1945 this->chdir_arg0);
1946 logindex++;
1947 this->chdir_arg0 = 0;
1948 }
1949 syscall::chdir:return /trackedpid[pid] == 1/ {
1950 self->chdir_arg0 = 0;
1951 }
1952 """
1953
1954 # execve-specific code, tends to throw a lot of exceptions.
1955 D_CODE_EXECVE = """
1956 /* Finally what we care about! */
1957 syscall::exec*:entry /trackedpid[pid]/ {
1958 self->exec_arg0 = copyinstr(arg0);
1959 /* Incrementally probe for a NULL in the argv parameter of execve() to
1960 * figure out argc. */
1961 /* TODO(maruel): Skip the remaining copyin() when a NULL pointer was
1962 * found. */
1963 self->exec_argc = 0;
1964 /* Probe for argc==1 */
1965 this->exec_argv = (user_addr_t*)copyin(
1966 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1967 self->exec_argc = this->exec_argv[self->exec_argc] ?
1968 (self->exec_argc + 1) : self->exec_argc;
1969
1970 /* Probe for argc==2 */
1971 this->exec_argv = (user_addr_t*)copyin(
1972 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1973 self->exec_argc = this->exec_argv[self->exec_argc] ?
1974 (self->exec_argc + 1) : self->exec_argc;
1975
1976 /* Probe for argc==3 */
1977 this->exec_argv = (user_addr_t*)copyin(
1978 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1979 self->exec_argc = this->exec_argv[self->exec_argc] ?
1980 (self->exec_argc + 1) : self->exec_argc;
1981
1982 /* Probe for argc==4 */
1983 this->exec_argv = (user_addr_t*)copyin(
1984 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1985 self->exec_argc = this->exec_argv[self->exec_argc] ?
1986 (self->exec_argc + 1) : self->exec_argc;
1987
1988 /* Copy the inputs strings since there is no guarantee they'll be
1989 * present after the call completed. */
1990 self->exec_argv0 = (self->exec_argc > 0) ?
1991 copyinstr(this->exec_argv[0]) : "";
1992 self->exec_argv1 = (self->exec_argc > 1) ?
1993 copyinstr(this->exec_argv[1]) : "";
1994 self->exec_argv2 = (self->exec_argc > 2) ?
1995 copyinstr(this->exec_argv[2]) : "";
1996 self->exec_argv3 = (self->exec_argc > 3) ?
1997 copyinstr(this->exec_argv[3]) : "";
1998 this->exec_argv = 0;
1999 }
2000 syscall::exec*:return /trackedpid[pid] && errno == 0/ {
2001 /* We need to join strings here, as using multiple printf() would
2002 * cause tearing when multiple threads/processes are traced.
2003 * Since it is impossible to escape a string and join it to another one,
2004 * like sprintf("%s%S", previous, more), use hackery.
2005 * Each of the elements are split with a \\1. \\0 cannot be used because
2006 * it is simply ignored. This will conflict with any program putting a
2007 * \\1 in their execve() string but this should be "rare enough" */
2008 this->args = "";
2009 /* Process exec_argv[0] */
2010 this->args = strjoin(
2011 this->args, (self->exec_argc > 0) ? self->exec_argv0 : "");
2012
2013 /* Process exec_argv[1] */
2014 this->args = strjoin(
2015 this->args, (self->exec_argc > 1) ? "\\1" : "");
2016 this->args = strjoin(
2017 this->args, (self->exec_argc > 1) ? self->exec_argv1 : "");
2018
2019 /* Process exec_argv[2] */
2020 this->args = strjoin(
2021 this->args, (self->exec_argc > 2) ? "\\1" : "");
2022 this->args = strjoin(
2023 this->args, (self->exec_argc > 2) ? self->exec_argv2 : "");
2024
2025 /* Process exec_argv[3] */
2026 this->args = strjoin(
2027 this->args, (self->exec_argc > 3) ? "\\1" : "");
2028 this->args = strjoin(
2029 this->args, (self->exec_argc > 3) ? self->exec_argv3 : "");
2030
2031 /* Prints self->exec_argc to permits verifying the internal
2032 * consistency since this code is quite fishy. */
2033 printf("%d %d %s(\\"%s\\", [%d, %S])\\n",
2034 logindex, pid, probefunc,
2035 self->exec_arg0,
2036 self->exec_argc,
2037 this->args);
2038 logindex++;
2039 this->args = 0;
2040 }
2041 syscall::exec*:return /trackedpid[pid]/ {
2042 self->exec_arg0 = 0;
2043 self->exec_argc = 0;
2044 self->exec_argv0 = 0;
2045 self->exec_argv1 = 0;
2046 self->exec_argv2 = 0;
2047 self->exec_argv3 = 0;
2048 }
2049 """
2050
2051 # Code currently not used.
2052 D_EXTRANEOUS = """
2053 /* This is a good learning experience, since it traces a lot of things
2054 * related to the process and child processes.
2055 * Warning: it generates a gigantic log. For example, tracing
2056 * "data/trace_inputs/child1.py --child" generates a 2mb log and takes
2057 * several minutes to execute.
2058 */
2059 /*
2060 mach_trap::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2061 printf("%d %d %s_%s() = %d\\n",
2062 logindex, pid, probeprov, probefunc, errno);
2063 logindex++;
2064 }
2065 proc::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2066 printf("%d %d %s_%s() = %d\\n",
2067 logindex, pid, probeprov, probefunc, errno);
2068 logindex++;
2069 }
2070 sched::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2071 printf("%d %d %s_%s() = %d\\n",
2072 logindex, pid, probeprov, probefunc, errno);
2073 logindex++;
2074 }
2075 syscall::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2076 printf("%d %d %s_%s() = %d\\n",
2077 logindex, pid, probeprov, probefunc, errno);
2078 logindex++;
2079 }
2080 vminfo::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2081 printf("%d %d %s_%s() = %d\\n",
2082 logindex, pid, probeprov, probefunc, errno);
2083 logindex++;
2084 }
2085 */
2086 /* TODO(maruel): *stat* functions and friends
2087 syscall::access:return,
2088 syscall::chdir:return,
2089 syscall::chflags:return,
2090 syscall::chown:return,
2091 syscall::chroot:return,
2092 syscall::getattrlist:return,
2093 syscall::getxattr:return,
2094 syscall::lchown:return,
2095 syscall::lstat64:return,
2096 syscall::lstat:return,
2097 syscall::mkdir:return,
2098 syscall::pathconf:return,
2099 syscall::readlink:return,
2100 syscall::removexattr:return,
2101 syscall::setxattr:return,
2102 syscall::stat64:return,
2103 syscall::stat:return,
2104 syscall::truncate:return,
2105 syscall::unlink:return,
2106 syscall::utimes:return,
2107 */
2108 """
2109
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00002110 def __init__(self, logname, use_sudo):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002111 """Starts the log collection with dtrace.
2112
2113 Requires root access or chmod 4555 on dtrace. dtrace is asynchronous so
2114 this needs to wait for dtrace to be "warmed up".
2115 """
2116 super(Dtrace.Tracer, self).__init__(logname)
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002117 # This script is used as a signal to figure out the root process.
2118 self._signal_script = create_subprocess_thunk()
2119 self._scripts_to_cleanup.append(self._signal_script)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002120 # This unique dummy temp file is used to signal the dtrace script that it
2121 # should stop as soon as all the child processes are done. A bit hackish
2122 # but works fine enough.
2123 self._dummy_file_id, self._dummy_file_name = tempfile.mkstemp(
2124 prefix='trace_signal_file')
2125
maruel@chromium.org9b2112a2013-04-05 18:01:02 +00002126 dtrace_path = '/usr/sbin/dtrace'
2127 if not os.path.isfile(dtrace_path):
2128 dtrace_path = 'dtrace'
2129 elif use_sudo is None and (os.stat(dtrace_path).st_mode & stat.S_ISUID):
2130 # No need to sudo. For those following at home, don't do that.
2131 use_sudo = False
2132
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002133 # Note: do not use the -p flag. It's useless if the initial process quits
2134 # too fast, resulting in missing traces from the grand-children. The D
2135 # code manages the dtrace lifetime itself.
2136 trace_cmd = [
maruel@chromium.org9b2112a2013-04-05 18:01:02 +00002137 dtrace_path,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002138 # Use a larger buffer if getting 'out of scratch space' errors.
2139 # Ref: https://wikis.oracle.com/display/DTrace/Options+and+Tunables
2140 '-b', '10m',
2141 '-x', 'dynvarsize=10m',
2142 #'-x', 'dtrace_global_maxsize=1m',
2143 '-x', 'evaltime=exec',
2144 '-o', '/dev/stderr',
2145 '-q',
2146 '-n', self._get_dtrace_code(),
2147 ]
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00002148 if use_sudo is not False:
2149 trace_cmd.insert(0, 'sudo')
2150
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002151 with open(self._logname + '.log', 'wb') as logfile:
2152 self._dtrace = subprocess.Popen(
2153 trace_cmd, stdout=logfile, stderr=subprocess.STDOUT)
2154 logging.debug('Started dtrace pid: %d' % self._dtrace.pid)
2155
2156 # Reads until one line is printed, which signifies dtrace is up and ready.
2157 with open(self._logname + '.log', 'rb') as logfile:
2158 while 'dtrace_BEGIN' not in logfile.readline():
2159 if self._dtrace.poll() is not None:
2160 # Do a busy wait. :/
2161 break
2162 logging.debug('dtrace started')
2163
2164 def _get_dtrace_code(self):
2165 """Setups the D code to implement child process tracking.
2166
2167 Injects the cookie in the script so it knows when to stop.
2168
2169 The script will detect any instance of the script created with
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002170 create_subprocess_thunk() and will start tracing it.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002171 """
maruel@chromium.orgbe75f282012-11-02 19:34:37 +00002172 out = (
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002173 'inline int PID = %d;\n'
2174 'inline string SCRIPT = "%s";\n'
2175 'inline int FILE_ID = %d;\n'
2176 '\n'
2177 '%s') % (
2178 os.getpid(),
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002179 self._signal_script,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002180 self._dummy_file_id,
maruel@chromium.orgbe75f282012-11-02 19:34:37 +00002181 self.D_CODE)
2182 if os.environ.get('TRACE_INPUTS_DTRACE_ENABLE_EXECVE') == '1':
2183 # Do not enable by default since it tends to spew dtrace: error lines
2184 # because the execve() parameters are not in valid memory at the time of
2185 # logging.
2186 # TODO(maruel): Find a way to make this reliable since it's useful but
2187 # only works in limited/trivial uses cases for now.
2188 out += self.D_CODE_EXECVE
2189 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002190
2191 def trace(self, cmd, cwd, tracename, output):
2192 """Runs dtrace on an executable.
2193
2194 This dtruss is broken when it starts the process itself or when tracing
2195 child processes, this code starts a wrapper process
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002196 generated with create_subprocess_thunk() which starts the executable to
2197 trace.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002198 """
2199 logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
2200 assert os.path.isabs(cmd[0]), cmd[0]
2201 assert os.path.isabs(cwd), cwd
2202 assert os.path.normpath(cwd) == cwd, cwd
2203 with self._lock:
2204 if not self._initialized:
2205 raise TracingFailure(
2206 'Called Tracer.trace() on an unitialized object',
2207 None, None, None, tracename)
2208 assert tracename not in (i['trace'] for i in self._traces)
2209
2210 # Starts the script wrapper to start the child process. This signals the
2211 # dtrace script that this process is to be traced.
2212 stdout = stderr = None
2213 if output:
2214 stdout = subprocess.PIPE
2215 stderr = subprocess.STDOUT
2216 child_cmd = [
2217 sys.executable,
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002218 self._signal_script,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002219 tracename,
2220 ]
2221 # Call a dummy function so that dtrace knows I'm about to launch a process
2222 # that needs to be traced.
2223 # Yummy.
2224 child = subprocess.Popen(
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002225 child_cmd + tools.fix_python_path(cmd),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002226 stdin=subprocess.PIPE,
2227 stdout=stdout,
2228 stderr=stderr,
2229 cwd=cwd)
2230 logging.debug('Started child pid: %d' % child.pid)
2231
2232 out = child.communicate()[0]
2233 # This doesn't mean tracing is done, one of the grand-child process may
2234 # still be alive. It will be tracked with the dtrace script.
2235
2236 with self._lock:
2237 assert tracename not in (i['trace'] for i in self._traces)
2238 self._traces.append(
2239 {
2240 'cmd': cmd,
2241 'cwd': cwd,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002242 'pid': child.pid,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002243 'output': out,
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002244 'trace': tracename,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002245 })
2246 return child.returncode, out
2247
2248 def close(self, timeout=None):
2249 """Terminates dtrace."""
2250 logging.debug('close(%s)' % timeout)
2251 try:
2252 try:
2253 super(Dtrace.Tracer, self).close(timeout)
2254 # Signal dtrace that it should stop now.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00002255 # ftruncate doesn't exist on Windows.
2256 os.ftruncate(self._dummy_file_id, 0) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002257 if timeout:
2258 start = time.time()
2259 # Use polling. :/
2260 while (self._dtrace.poll() is None and
2261 (time.time() - start) < timeout):
2262 time.sleep(0.1)
2263 self._dtrace.kill()
2264 self._dtrace.wait()
2265 finally:
2266 # Make sure to kill it in any case.
2267 if self._dtrace.poll() is None:
2268 try:
2269 self._dtrace.kill()
2270 self._dtrace.wait()
2271 except OSError:
2272 pass
2273
2274 if self._dtrace.returncode != 0:
2275 # Warn about any dtrace failure but basically ignore it.
2276 print 'dtrace failure: %s' % self._dtrace.returncode
2277 finally:
2278 os.close(self._dummy_file_id)
2279 os.remove(self._dummy_file_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002280
2281 def post_process_log(self):
2282 """Sorts the log back in order when each call occured.
2283
2284 dtrace doesn't save the buffer in strict order since it keeps one buffer
2285 per CPU.
2286 """
2287 super(Dtrace.Tracer, self).post_process_log()
2288 logname = self._logname + '.log'
2289 with open(logname, 'rb') as logfile:
2290 lines = [l for l in logfile if l.strip()]
2291 errors = [l for l in lines if l.startswith('dtrace:')]
2292 if errors:
2293 raise TracingFailure(
2294 'Found errors in the trace: %s' % '\n'.join(errors),
2295 None, None, None, logname)
2296 try:
2297 lines = sorted(lines, key=lambda l: int(l.split(' ', 1)[0]))
2298 except ValueError:
2299 raise TracingFailure(
2300 'Found errors in the trace: %s' % '\n'.join(
2301 l for l in lines if l.split(' ', 1)[0].isdigit()),
2302 None, None, None, logname)
2303 with open(logname, 'wb') as logfile:
2304 logfile.write(''.join(lines))
2305
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00002306 def __init__(self, use_sudo=None):
2307 super(Dtrace, self).__init__()
2308 self.use_sudo = use_sudo
2309
2310 def get_tracer(self, logname):
2311 return self.Tracer(logname, self.use_sudo)
2312
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002313 @staticmethod
2314 def clean_trace(logname):
2315 for ext in ('', '.log'):
2316 if os.path.isfile(logname + ext):
2317 os.remove(logname + ext)
2318
2319 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00002320 def parse_log(cls, logname, blacklist, trace_name):
2321 logging.info('parse_log(%s, ..., %s)', logname, trace_name)
maruel@chromium.org8b056ba2012-10-16 14:04:49 +00002322 assert os.path.isabs(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002323
2324 def blacklist_more(filepath):
2325 # All the HFS metadata is in the form /.vol/...
2326 return blacklist(filepath) or re.match(r'^\/\.vol\/.+$', filepath)
2327
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05002328 data = tools.read_json(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002329 out = []
2330 for item in data['traces']:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00002331 if trace_name and item['trace'] != trace_name:
2332 continue
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002333 result = {
2334 'output': item['output'],
2335 'trace': item['trace'],
2336 }
2337 try:
2338 context = cls.Context(blacklist_more, item['pid'], item['cwd'])
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002339 # It's fine to assume the file as UTF-8: OSX enforces the file names to
2340 # be valid UTF-8 and we control the log output.
2341 for line in codecs.open(logname + '.log', 'rb', encoding='utf-8'):
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002342 context.on_line(line)
2343 result['results'] = context.to_results()
2344 except TracingFailure:
2345 result['exception'] = sys.exc_info()
2346 out.append(result)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002347 return out
2348
2349
2350class LogmanTrace(ApiBase):
2351 """Uses the native Windows ETW based tracing functionality to trace a child
2352 process.
2353
2354 Caveat: this implementations doesn't track cwd or initial_cwd. It is because
2355 the Windows Kernel doesn't have a concept of 'current working directory' at
2356 all. A Win32 process has a map of current directories, one per drive letter
2357 and it is managed by the user mode kernel32.dll. In kernel, a file is always
2358 opened relative to another file_object or as an absolute path. All the current
2359 working directory logic is done in user mode.
2360 """
2361 class Context(ApiBase.Context):
2362 """Processes a ETW log line and keeps the list of existent and non
2363 existent files accessed.
2364
2365 Ignores directories.
2366 """
2367 # These indexes are for the stripped version in json.
2368 EVENT_NAME = 0
2369 TYPE = 1
2370 PID = 2
2371 TID = 3
2372 PROCESSOR_ID = 4
2373 TIMESTAMP = 5
2374 USER_DATA = 6
2375
2376 class Process(ApiBase.Context.Process):
2377 def __init__(self, *args):
2378 super(LogmanTrace.Context.Process, self).__init__(*args)
2379 # Handle file objects that succeeded.
2380 self.file_objects = {}
2381
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002382 def __init__(self, blacklist, thunk_pid, trace_name, thunk_cmd):
2383 logging.info(
2384 '%s(%d, %s, %s)', self.__class__.__name__, thunk_pid, trace_name,
2385 thunk_cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002386 super(LogmanTrace.Context, self).__init__(blacklist)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002387 self._drive_map = file_path.DosDriveMap()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002388 # Threads mapping to the corresponding process id.
2389 self._threads_active = {}
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002390 # Process ID of the tracer, e.g. the temporary script created by
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002391 # create_subprocess_thunk(). This is tricky because the process id may
2392 # have been reused.
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002393 self._thunk_pid = thunk_pid
2394 self._thunk_cmd = thunk_cmd
2395 self._trace_name = trace_name
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002396 self._line_number = 0
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002397 self._thunk_process = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002398
2399 def on_line(self, line):
2400 """Processes a json Event line."""
2401 self._line_number += 1
2402 try:
2403 # By Opcode
2404 handler = getattr(
2405 self,
2406 'handle_%s_%s' % (line[self.EVENT_NAME], line[self.TYPE]),
2407 None)
2408 if not handler:
2409 raise TracingFailure(
2410 'Unexpected event %s_%s' % (
2411 line[self.EVENT_NAME], line[self.TYPE]),
2412 None, None, None)
2413 handler(line)
2414 except TracingFailure, e:
2415 # Hack in the values since the handler could be a static function.
2416 e.pid = line[self.PID]
2417 e.line = line
2418 e.line_number = self._line_number
2419 # Re-raise the modified exception.
2420 raise
2421 except (KeyError, NotImplementedError, ValueError), e:
2422 raise TracingFailure(
2423 'Trace generated a %s exception: %s' % (
2424 e.__class__.__name__, str(e)),
2425 line[self.PID],
2426 self._line_number,
2427 line,
2428 e)
2429
2430 def to_results(self):
2431 if not self.root_process:
2432 raise TracingFailure(
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002433 'Failed to detect the initial process %d' % self._thunk_pid,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002434 None, None, None)
2435 process = self.root_process.to_results_process()
2436 return Results(process)
2437
2438 def _thread_to_process(self, tid):
2439 """Finds the process from the thread id."""
2440 tid = int(tid, 16)
2441 pid = self._threads_active.get(tid)
2442 if not pid or not self._process_lookup.get(pid):
2443 return
2444 return self._process_lookup[pid]
2445
2446 @classmethod
2447 def handle_EventTrace_Header(cls, line):
2448 """Verifies no event was dropped, e.g. no buffer overrun occured."""
2449 BUFFER_SIZE = cls.USER_DATA
2450 #VERSION = cls.USER_DATA + 1
2451 #PROVIDER_VERSION = cls.USER_DATA + 2
2452 #NUMBER_OF_PROCESSORS = cls.USER_DATA + 3
2453 #END_TIME = cls.USER_DATA + 4
2454 #TIMER_RESOLUTION = cls.USER_DATA + 5
2455 #MAX_FILE_SIZE = cls.USER_DATA + 6
2456 #LOG_FILE_MODE = cls.USER_DATA + 7
2457 #BUFFERS_WRITTEN = cls.USER_DATA + 8
2458 #START_BUFFERS = cls.USER_DATA + 9
2459 #POINTER_SIZE = cls.USER_DATA + 10
2460 EVENTS_LOST = cls.USER_DATA + 11
2461 #CPU_SPEED = cls.USER_DATA + 12
2462 #LOGGER_NAME = cls.USER_DATA + 13
2463 #LOG_FILE_NAME = cls.USER_DATA + 14
2464 #BOOT_TIME = cls.USER_DATA + 15
2465 #PERF_FREQ = cls.USER_DATA + 16
2466 #START_TIME = cls.USER_DATA + 17
2467 #RESERVED_FLAGS = cls.USER_DATA + 18
2468 #BUFFERS_LOST = cls.USER_DATA + 19
2469 #SESSION_NAME_STRING = cls.USER_DATA + 20
2470 #LOG_FILE_NAME_STRING = cls.USER_DATA + 21
2471 if line[EVENTS_LOST] != '0':
2472 raise TracingFailure(
2473 ( '%s events were lost during trace, please increase the buffer '
2474 'size from %s') % (line[EVENTS_LOST], line[BUFFER_SIZE]),
2475 None, None, None)
2476
2477 def handle_FileIo_Cleanup(self, line):
2478 """General wisdom: if a file is closed, it's because it was opened.
2479
2480 Note that FileIo_Close is not used since if a file was opened properly but
2481 not closed before the process exits, only Cleanup will be logged.
2482 """
2483 #IRP = self.USER_DATA
2484 TTID = self.USER_DATA + 1 # Thread ID, that's what we want.
2485 FILE_OBJECT = self.USER_DATA + 2
2486 #FILE_KEY = self.USER_DATA + 3
2487 proc = self._thread_to_process(line[TTID])
2488 if not proc:
2489 # Not a process we care about.
2490 return
2491 file_object = line[FILE_OBJECT]
2492 if file_object in proc.file_objects:
maruel@chromium.org538141b2013-06-03 20:57:17 +00002493 filepath, access_type = proc.file_objects.pop(file_object)
2494 proc.add_file(filepath, access_type)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002495
2496 def handle_FileIo_Create(self, line):
2497 """Handles a file open.
2498
2499 All FileIo events are described at
2500 http://msdn.microsoft.com/library/windows/desktop/aa363884.aspx
2501 for some value of 'description'.
2502
2503 " (..) process and thread id values of the IO events (..) are not valid "
2504 http://msdn.microsoft.com/magazine/ee358703.aspx
2505
2506 The FileIo.Create event doesn't return if the CreateFile() call
2507 succeeded, so keep track of the file_object and check that it is
2508 eventually closed with FileIo_Cleanup.
2509 """
2510 #IRP = self.USER_DATA
2511 TTID = self.USER_DATA + 1 # Thread ID, that's what we want.
2512 FILE_OBJECT = self.USER_DATA + 2
2513 #CREATE_OPTIONS = self.USER_DATA + 3
2514 #FILE_ATTRIBUTES = self.USER_DATA + 4
maruel@chromium.org538141b2013-06-03 20:57:17 +00002515 #SHARE_ACCESS = self.USER_DATA + 5
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002516 OPEN_PATH = self.USER_DATA + 6
2517
2518 proc = self._thread_to_process(line[TTID])
2519 if not proc:
2520 # Not a process we care about.
2521 return
2522
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002523 raw_path = line[OPEN_PATH]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002524 # Ignore directories and bare drive right away.
2525 if raw_path.endswith(os.path.sep):
2526 return
2527 filepath = self._drive_map.to_win32(raw_path)
2528 # Ignore bare drive right away. Some may still fall through with format
2529 # like '\\?\X:'
2530 if len(filepath) == 2:
2531 return
2532 file_object = line[FILE_OBJECT]
2533 if os.path.isdir(filepath):
2534 # There is no O_DIRECTORY equivalent on Windows. The closed is
2535 # FILE_FLAG_BACKUP_SEMANTICS but it's not exactly right either. So
2536 # simply discard directories are they are found.
2537 return
maruel@chromium.org538141b2013-06-03 20:57:17 +00002538 # Override any stale file object.
2539 # TODO(maruel): Figure out a way to detect if the file was opened for
2540 # reading or writting. Sadly CREATE_OPTIONS doesn't seem to be of any help
2541 # here. For now mark as None to make it clear we have no idea what it is
2542 # about.
2543 proc.file_objects[file_object] = (filepath, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002544
2545 def handle_FileIo_Rename(self, line):
2546 # TODO(maruel): Handle?
2547 pass
2548
2549 def handle_Process_End(self, line):
2550 pid = line[self.PID]
2551 if self._process_lookup.get(pid):
2552 logging.info('Terminated: %d' % pid)
2553 self._process_lookup[pid] = None
2554 else:
2555 logging.debug('Terminated: %d' % pid)
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002556 if self._thunk_process and self._thunk_process.pid == pid:
2557 self._thunk_process = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002558
2559 def handle_Process_Start(self, line):
2560 """Handles a new child process started by PID."""
2561 #UNIQUE_PROCESS_KEY = self.USER_DATA
2562 PROCESS_ID = self.USER_DATA + 1
2563 #PARENT_PID = self.USER_DATA + 2
2564 #SESSION_ID = self.USER_DATA + 3
2565 #EXIT_STATUS = self.USER_DATA + 4
2566 #DIRECTORY_TABLE_BASE = self.USER_DATA + 5
2567 #USER_SID = self.USER_DATA + 6
2568 IMAGE_FILE_NAME = self.USER_DATA + 7
2569 COMMAND_LINE = self.USER_DATA + 8
2570
2571 ppid = line[self.PID]
2572 pid = int(line[PROCESS_ID], 16)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002573 command_line = CommandLineToArgvW(line[COMMAND_LINE])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002574 logging.debug(
2575 'New process %d->%d (%s) %s' %
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002576 (ppid, pid, line[IMAGE_FILE_NAME], command_line))
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002577
2578 if pid == self._thunk_pid:
2579 # Need to ignore processes we don't know about because the log is
2580 # system-wide. self._thunk_pid shall start only one process.
2581 # This is tricky though because Windows *loves* to reuse process id and
2582 # it happens often that the process ID of the thunk script created by
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002583 # create_subprocess_thunk() is reused. So just detecting the pid here is
2584 # not sufficient, we must confirm the command line.
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002585 if command_line[:len(self._thunk_cmd)] != self._thunk_cmd:
2586 logging.info(
2587 'Ignoring duplicate pid %d for %s: %s while searching for %s',
2588 pid, self._trace_name, command_line, self._thunk_cmd)
2589 return
2590
2591 # TODO(maruel): The check is quite weak. Add the thunk path.
2592 if self._thunk_process:
2593 raise TracingFailure(
2594 ( 'Parent process is _thunk_pid(%d) but thunk_process(%d) is '
2595 'already set') % (self._thunk_pid, self._thunk_process.pid),
2596 None, None, None)
2597 proc = self.Process(self.blacklist, pid, None)
2598 self._thunk_process = proc
2599 return
2600 elif ppid == self._thunk_pid and self._thunk_process:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002601 proc = self.Process(self.blacklist, pid, None)
2602 self.root_process = proc
2603 ppid = None
2604 elif self._process_lookup.get(ppid):
2605 proc = self.Process(self.blacklist, pid, None)
2606 self._process_lookup[ppid].children.append(proc)
2607 else:
2608 # Ignore
2609 return
2610 self._process_lookup[pid] = proc
2611
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002612 proc.command = command_line
2613 proc.executable = line[IMAGE_FILE_NAME]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002614 # proc.command[0] may be the absolute path of 'executable' but it may be
2615 # anything else too. If it happens that command[0] ends with executable,
2616 # use it, otherwise defaults to the base name.
2617 cmd0 = proc.command[0].lower()
2618 if not cmd0.endswith('.exe'):
2619 # TODO(maruel): That's not strictly true either.
2620 cmd0 += '.exe'
2621 if cmd0.endswith(proc.executable) and os.path.isfile(cmd0):
2622 # Fix the path.
2623 cmd0 = cmd0.replace('/', os.path.sep)
2624 cmd0 = os.path.normpath(cmd0)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002625 proc.executable = file_path.get_native_path_case(cmd0)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002626 logging.info(
2627 'New child: %s -> %d %s' % (ppid, pid, proc.executable))
2628
2629 def handle_Thread_End(self, line):
2630 """Has the same parameters as Thread_Start."""
2631 tid = int(line[self.TID], 16)
2632 self._threads_active.pop(tid, None)
2633
2634 def handle_Thread_Start(self, line):
2635 """Handles a new thread created.
2636
2637 Do not use self.PID here since a process' initial thread is created by
2638 the parent process.
2639 """
2640 PROCESS_ID = self.USER_DATA
2641 TTHREAD_ID = self.USER_DATA + 1
2642 #STACK_BASE = self.USER_DATA + 2
2643 #STACK_LIMIT = self.USER_DATA + 3
2644 #USER_STACK_BASE = self.USER_DATA + 4
2645 #USER_STACK_LIMIT = self.USER_DATA + 5
2646 #AFFINITY = self.USER_DATA + 6
2647 #WIN32_START_ADDR = self.USER_DATA + 7
2648 #TEB_BASE = self.USER_DATA + 8
2649 #SUB_PROCESS_TAG = self.USER_DATA + 9
2650 #BASE_PRIORITY = self.USER_DATA + 10
2651 #PAGE_PRIORITY = self.USER_DATA + 11
2652 #IO_PRIORITY = self.USER_DATA + 12
2653 #THREAD_FLAGS = self.USER_DATA + 13
2654 # Do not use self.PID here since a process' initial thread is created by
2655 # the parent process.
2656 pid = int(line[PROCESS_ID], 16)
2657 tid = int(line[TTHREAD_ID], 16)
2658 logging.debug('New thread pid:%d, tid:%d' % (pid, tid))
2659 self._threads_active[tid] = pid
2660
2661 @classmethod
2662 def supported_events(cls):
2663 """Returns all the procesed events."""
2664 out = []
2665 for member in dir(cls):
2666 match = re.match(r'^handle_([A-Za-z]+)_([A-Za-z]+)$', member)
2667 if match:
2668 out.append(match.groups())
2669 return out
2670
2671 class Tracer(ApiBase.Tracer):
2672 # The basic headers.
2673 EXPECTED_HEADER = [
2674 u'Event Name',
2675 u'Type',
2676 u'Event ID',
2677 u'Version',
2678 u'Channel',
2679 u'Level', # 5
2680 u'Opcode',
2681 u'Task',
2682 u'Keyword',
2683 u'PID',
2684 u'TID', # 10
2685 u'Processor Number',
2686 u'Instance ID',
2687 u'Parent Instance ID',
2688 u'Activity ID',
2689 u'Related Activity ID', # 15
2690 u'Clock-Time',
2691 u'Kernel(ms)', # Both have a resolution of ~15ms which makes them
2692 u'User(ms)', # pretty much useless.
2693 u'User Data', # Extra arguments that are event-specific.
2694 ]
2695 # Only the useful headers common to all entries are listed there. Any column
2696 # at 19 or higher is dependent on the specific event.
2697 EVENT_NAME = 0
2698 TYPE = 1
2699 PID = 9
2700 TID = 10
2701 PROCESSOR_ID = 11
2702 TIMESTAMP = 16
2703 NULL_GUID = '{00000000-0000-0000-0000-000000000000}'
2704 USER_DATA = 19
2705
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002706 class CsvReader(object):
2707 """CSV reader that reads files generated by tracerpt.exe.
2708
2709 csv.reader() fails to read them properly, it mangles file names quoted
2710 with "" with a comma in it.
2711 """
2712 # 0. Had a ',' or one of the following ' ' after a comma, next should
2713 # be ' ', '"' or string or ',' for an empty field.
2714 ( HAD_DELIMITER,
2715 # 1. Processing an unquoted field up to ','.
2716 IN_STR,
2717 # 2. Processing a new field starting with '"'.
2718 STARTING_STR_QUOTED,
2719 # 3. Second quote in a row at the start of a field. It could be either
2720 # '""foo""' or '""'. Who the hell thought it was a great idea to use
2721 # the same character for delimiting and escaping?
2722 STARTING_SECOND_QUOTE,
2723 # 4. A quote inside a quoted string where the previous character was
2724 # not a quote, so the string is not empty. Can be either: end of a
2725 # quoted string (a delimiter) or a quote escape. The next char must be
2726 # either '"' or ','.
2727 HAD_QUOTE_IN_QUOTED,
2728 # 5. Second quote inside a quoted string.
2729 HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED,
2730 # 6. Processing a field that started with '"'.
2731 IN_STR_QUOTED) = range(7)
2732
2733 def __init__(self, f):
2734 self.f = f
2735
2736 def __iter__(self):
2737 return self
2738
2739 def next(self):
2740 """Splits the line in fields."""
2741 line = self.f.readline()
2742 if not line:
2743 raise StopIteration()
2744 line = line.strip()
2745 fields = []
2746 state = self.HAD_DELIMITER
2747 for i, c in enumerate(line):
2748 if state == self.HAD_DELIMITER:
2749 if c == ',':
2750 # Empty field.
2751 fields.append('')
2752 elif c == ' ':
2753 # Ignore initial whitespaces
2754 pass
2755 elif c == '"':
2756 state = self.STARTING_STR_QUOTED
2757 fields.append('')
2758 else:
2759 # Start of a new field.
2760 state = self.IN_STR
2761 fields.append(c)
2762
2763 elif state == self.IN_STR:
2764 # Do not accept quote inside unquoted field.
2765 assert c != '"', (i, c, line, fields)
2766 if c == ',':
2767 fields[-1] = fields[-1].strip()
2768 state = self.HAD_DELIMITER
2769 else:
2770 fields[-1] = fields[-1] + c
2771
2772 elif state == self.STARTING_STR_QUOTED:
2773 if c == '"':
2774 # Do not store the character yet.
2775 state = self.STARTING_SECOND_QUOTE
2776 else:
2777 state = self.IN_STR_QUOTED
2778 fields[-1] = fields[-1] + c
2779
2780 elif state == self.STARTING_SECOND_QUOTE:
2781 if c == ',':
2782 # It was an empty field. '""' == ''.
2783 state = self.HAD_DELIMITER
2784 else:
2785 fields[-1] = fields[-1] + '"' + c
2786 state = self.IN_STR_QUOTED
2787
2788 elif state == self.HAD_QUOTE_IN_QUOTED:
2789 if c == ',':
2790 # End of the string.
2791 state = self.HAD_DELIMITER
2792 elif c == '"':
2793 state = self.HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED
2794 else:
2795 # The previous double-quote was just an unescaped quote.
2796 fields[-1] = fields[-1] + '"' + c
2797 state = self.IN_STR_QUOTED
2798
2799 elif state == self.HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED:
2800 if c == ',':
2801 # End of the string.
2802 state = self.HAD_DELIMITER
2803 fields[-1] = fields[-1] + '"'
2804 else:
maruel@chromium.org261d1352013-02-25 19:23:20 +00002805 # That's just how the logger rolls. Revert back to appending the
2806 # char and "guess" it was a quote in a double-quoted string.
2807 state = self.IN_STR_QUOTED
2808 fields[-1] = fields[-1] + '"' + c
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002809
2810 elif state == self.IN_STR_QUOTED:
2811 if c == '"':
2812 # Could be a delimiter or an escape.
2813 state = self.HAD_QUOTE_IN_QUOTED
2814 else:
2815 fields[-1] = fields[-1] + c
2816
2817 if state == self.HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED:
2818 fields[-1] = fields[-1] + '"'
2819 else:
2820 assert state in (
2821 # Terminated with a normal field.
2822 self.IN_STR,
2823 # Terminated with an empty field.
2824 self.STARTING_SECOND_QUOTE,
2825 # Terminated with a normal quoted field.
2826 self.HAD_QUOTE_IN_QUOTED), (
2827 line, state, fields)
2828 return fields
2829
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002830 def __init__(self, logname):
2831 """Starts the log collection.
2832
2833 Requires administrative access. logman.exe is synchronous so no need for a
2834 "warmup" call. 'Windows Kernel Trace' is *localized* so use its GUID
2835 instead. The GUID constant name is SystemTraceControlGuid. Lovely.
2836
2837 One can get the list of potentially interesting providers with:
2838 "logman query providers | findstr /i file"
2839 """
2840 super(LogmanTrace.Tracer, self).__init__(logname)
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002841 self._signal_script = create_subprocess_thunk()
2842 self._scripts_to_cleanup.append(self._signal_script)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002843 cmd_start = [
2844 'logman.exe',
2845 'start',
2846 'NT Kernel Logger',
2847 '-p', '{9e814aad-3204-11d2-9a82-006008a86939}',
2848 # splitio,fileiocompletion,syscall,file,cswitch,img
2849 '(process,fileio,thread)',
2850 '-o', self._logname + '.etl',
2851 '-ets', # Send directly to kernel
2852 # Values extracted out of thin air.
2853 # Event Trace Session buffer size in kb.
2854 '-bs', '10240',
2855 # Number of Event Trace Session buffers.
2856 '-nb', '16', '256',
2857 ]
2858 logging.debug('Running: %s' % cmd_start)
2859 try:
2860 subprocess.check_call(
2861 cmd_start,
2862 stdin=subprocess.PIPE,
2863 stdout=subprocess.PIPE,
2864 stderr=subprocess.STDOUT)
2865 except subprocess.CalledProcessError, e:
2866 if e.returncode == -2147024891:
2867 print >> sys.stderr, 'Please restart with an elevated admin prompt'
2868 elif e.returncode == -2144337737:
2869 print >> sys.stderr, (
2870 'A kernel trace was already running, stop it and try again')
2871 raise
2872
2873 def trace(self, cmd, cwd, tracename, output):
2874 logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
2875 assert os.path.isabs(cmd[0]), cmd[0]
2876 assert os.path.isabs(cwd), cwd
2877 assert os.path.normpath(cwd) == cwd, cwd
2878 with self._lock:
2879 if not self._initialized:
2880 raise TracingFailure(
2881 'Called Tracer.trace() on an unitialized object',
2882 None, None, None, tracename)
2883 assert tracename not in (i['trace'] for i in self._traces)
2884
2885 # Use "logman -?" for help.
2886
2887 stdout = stderr = None
2888 if output:
2889 stdout = subprocess.PIPE
2890 stderr = subprocess.STDOUT
2891
2892 # Run the child process.
2893 logging.debug('Running: %s' % cmd)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002894 # Use the temporary script generated with create_subprocess_thunk() so we
2895 # have a clear pid owner. Since trace_inputs.py can be used as a library
2896 # and could trace multiple processes simultaneously, it makes it more
2897 # complex if the executable to be traced is executed directly here. It
2898 # also solves issues related to logman.exe that needs to be executed to
2899 # control the kernel trace.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002900 child_cmd = [
2901 sys.executable,
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002902 self._signal_script,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002903 tracename,
2904 ]
2905 child = subprocess.Popen(
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002906 child_cmd + tools.fix_python_path(cmd),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002907 cwd=cwd,
2908 stdin=subprocess.PIPE,
2909 stdout=stdout,
2910 stderr=stderr)
2911 logging.debug('Started child pid: %d' % child.pid)
2912 out = child.communicate()[0]
2913 # This doesn't mean all the grand-children are done. Sadly, we don't have
2914 # a good way to determine that.
2915
2916 with self._lock:
2917 assert tracename not in (i['trace'] for i in self._traces)
2918 self._traces.append({
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00002919 'cmd': cmd,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002920 'cwd': cwd,
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002921 'output': out,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002922 'pid': child.pid,
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002923 # Used to figure out the real process when process ids are reused.
2924 'thunk_cmd': child_cmd,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002925 'trace': tracename,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002926 })
2927
2928 return child.returncode, out
2929
2930 def close(self, _timeout=None):
2931 """Stops the kernel log collection and converts the traces to text
2932 representation.
2933 """
2934 with self._lock:
maruel@chromium.org808e3f22013-05-16 16:42:54 +00002935 try:
2936 super(LogmanTrace.Tracer, self).close()
2937 finally:
2938 cmd_stop = [
2939 'logman.exe',
2940 'stop',
2941 'NT Kernel Logger',
2942 '-ets', # Sends the command directly to the kernel.
2943 ]
2944 logging.debug('Running: %s' % cmd_stop)
2945 subprocess.check_call(
2946 cmd_stop,
2947 stdin=subprocess.PIPE,
2948 stdout=subprocess.PIPE,
2949 stderr=subprocess.STDOUT)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002950
2951 def post_process_log(self):
2952 """Converts the .etl file into .csv then into .json."""
2953 super(LogmanTrace.Tracer, self).post_process_log()
2954 logformat = 'csv'
2955 self._convert_log(logformat)
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00002956 self._trim_log(logformat)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002957
maruel@chromium.org808e3f22013-05-16 16:42:54 +00002958 def _gen_logdata(self):
2959 return {
2960 'format': 'csv',
2961 'traces': self._traces,
2962 }
2963
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00002964 def _trim_log(self, logformat):
2965 """Reduces the amount of data in original log by generating a 'reduced'
2966 log.
2967 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002968 if logformat == 'csv_utf16':
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002969 file_handle = codecs.open(
2970 self._logname + '.' + logformat, 'r', encoding='utf-16')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002971
2972 elif logformat == 'csv':
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002973 assert sys.getfilesystemencoding() == 'mbcs'
2974 file_handle = codecs.open(
2975 self._logname + '.' + logformat, 'r',
2976 encoding=get_current_encoding())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002977
2978 supported_events = LogmanTrace.Context.supported_events()
2979
2980 def trim(generator):
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00002981 """Loads items from the generator and returns the interesting data.
2982
2983 It filters out any uninteresting line and reduce the amount of data in
2984 the trace.
2985 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002986 for index, line in enumerate(generator):
2987 if not index:
2988 if line != self.EXPECTED_HEADER:
2989 raise TracingFailure(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002990 'Found malformed header: %s' % line,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002991 None, None, None)
2992 continue
2993 # As you can see, the CSV is full of useful non-redundant information:
2994 if (line[2] != '0' or # Event ID
2995 line[3] not in ('2', '3') or # Version
2996 line[4] != '0' or # Channel
2997 line[5] != '0' or # Level
2998 line[7] != '0' or # Task
2999 line[8] != '0x0000000000000000' or # Keyword
3000 line[12] != '' or # Instance ID
3001 line[13] != '' or # Parent Instance ID
3002 line[14] != self.NULL_GUID or # Activity ID
3003 line[15] != ''): # Related Activity ID
3004 raise TracingFailure(
3005 'Found unexpected values in line: %s' % ' '.join(line),
3006 None, None, None)
3007
3008 if (line[self.EVENT_NAME], line[self.TYPE]) not in supported_events:
3009 continue
3010
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003011 yield [
3012 line[self.EVENT_NAME],
3013 line[self.TYPE],
3014 line[self.PID],
3015 line[self.TID],
3016 line[self.PROCESSOR_ID],
3017 line[self.TIMESTAMP],
3018 ] + line[self.USER_DATA:]
3019
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003020 # must not convert the trim() call into a list, since it will use too much
3021 # memory for large trace. use a csv file as a workaround since the json
3022 # parser requires a complete in-memory file.
3023 with open('%s.preprocessed' % self._logname, 'wb') as f:
3024 # $ and * can't be used in file name on windows, reducing the likelihood
3025 # of having to escape a string.
3026 out = csv.writer(
3027 f, delimiter='$', quotechar='*', quoting=csv.QUOTE_MINIMAL)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00003028 for line in trim(self.CsvReader(file_handle)):
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003029 out.writerow([s.encode('utf-8') for s in line])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003030
3031 def _convert_log(self, logformat):
3032 """Converts the ETL trace to text representation.
3033
3034 Normally, 'csv' is sufficient. If complex scripts are used (like eastern
3035 languages), use 'csv_utf16'. If localization gets in the way, use 'xml'.
3036
3037 Arguments:
3038 - logformat: Text format to be generated, csv, csv_utf16 or xml.
3039
3040 Use "tracerpt -?" for help.
3041 """
3042 LOCALE_INVARIANT = 0x7F
3043 windll.kernel32.SetThreadLocale(LOCALE_INVARIANT)
3044 cmd_convert = [
3045 'tracerpt.exe',
3046 '-l', self._logname + '.etl',
3047 '-o', self._logname + '.' + logformat,
3048 '-gmt', # Use UTC
3049 '-y', # No prompt
3050 # Use -of XML to get the header of each items after column 19, e.g. all
3051 # the actual headers of 'User Data'.
3052 ]
3053
3054 if logformat == 'csv':
3055 # tracerpt localizes the 'Type' column, for major brainfuck
3056 # entertainment. I can't imagine any sane reason to do that.
3057 cmd_convert.extend(['-of', 'CSV'])
3058 elif logformat == 'csv_utf16':
3059 # This causes it to use UTF-16, which doubles the log size but ensures
3060 # the log is readable for non-ASCII characters.
3061 cmd_convert.extend(['-of', 'CSV', '-en', 'Unicode'])
3062 elif logformat == 'xml':
3063 cmd_convert.extend(['-of', 'XML'])
3064 else:
3065 raise ValueError('Unexpected log format \'%s\'' % logformat)
3066 logging.debug('Running: %s' % cmd_convert)
3067 # This can takes tens of minutes for large logs.
3068 # Redirects all output to stderr.
3069 subprocess.check_call(
3070 cmd_convert,
3071 stdin=subprocess.PIPE,
3072 stdout=sys.stderr,
3073 stderr=sys.stderr)
3074
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003075 def __init__(self, use_sudo=False): # pylint: disable=W0613
3076 super(LogmanTrace, self).__init__()
3077 # Ignore use_sudo. It's irrelevant on Windows but kept to simplify the API.
3078
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003079 @staticmethod
3080 def clean_trace(logname):
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003081 for ext in ('', '.csv', '.etl', '.json', '.xml', '.preprocessed'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003082 if os.path.isfile(logname + ext):
3083 os.remove(logname + ext)
3084
3085 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003086 def parse_log(cls, logname, blacklist, trace_name):
3087 logging.info('parse_log(%s, ..., %s)', logname, trace_name)
maruel@chromium.org8b056ba2012-10-16 14:04:49 +00003088 assert os.path.isabs(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003089
3090 def blacklist_more(filepath):
3091 # All the NTFS metadata is in the form x:\$EXTEND or stuff like that.
3092 return blacklist(filepath) or re.match(r'[A-Z]\:\\\$EXTEND', filepath)
3093
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003094 # Create a list of (Context, result_dict) tuples. This is necessary because
3095 # the csv file may be larger than the amount of available memory.
3096 contexes = [
3097 (
3098 cls.Context(
3099 blacklist_more, item['pid'], item['trace'], item['thunk_cmd']),
3100 {
3101 'output': item['output'],
3102 'trace': item['trace'],
3103 },
3104 )
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05003105 for item in tools.read_json(logname)['traces']
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003106 if not trace_name or item['trace'] == trace_name
3107 ]
3108
3109 # The log may be too large to fit in memory and it is not efficient to read
3110 # it multiple times, so multiplex the contexes instead, which is slightly
3111 # more awkward.
3112 with open('%s.preprocessed' % logname, 'rb') as f:
3113 lines = csv.reader(
3114 f, delimiter='$', quotechar='*', quoting=csv.QUOTE_MINIMAL)
3115 for encoded in lines:
3116 line = [s.decode('utf-8') for s in encoded]
3117 # Convert the PID in-place from hex.
3118 line[cls.Context.PID] = int(line[cls.Context.PID], 16)
3119 for context in contexes:
3120 if 'exception' in context[1]:
3121 continue
3122 try:
3123 context[0].on_line(line)
3124 except TracingFailure:
3125 context[1]['exception'] = sys.exc_info()
3126
3127 for context in contexes:
3128 if 'exception' in context[1]:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003129 continue
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003130 context[1]['results'] = context[0].to_results()
3131
3132 return [context[1] for context in contexes]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003133
3134
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003135def get_api(**kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003136 """Returns the correct implementation for the current OS."""
3137 if sys.platform == 'cygwin':
3138 raise NotImplementedError(
3139 'Not implemented for cygwin, start the script from Win32 python')
3140 flavors = {
3141 'win32': LogmanTrace,
3142 'darwin': Dtrace,
3143 'sunos5': Dtrace,
3144 'freebsd7': Dtrace,
3145 'freebsd8': Dtrace,
3146 }
3147 # Defaults to strace.
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003148 return flavors.get(sys.platform, Strace)(**kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003149
3150
3151def extract_directories(root_dir, files, blacklist):
3152 """Detects if all the files in a directory are in |files| and if so, replace
3153 the individual files by a Results.Directory instance.
3154
3155 Takes a list of Results.File instances and returns a shorter list of
3156 Results.File and Results.Directory instances.
3157
3158 Arguments:
3159 - root_dir: Optional base directory that shouldn't be search further.
3160 - files: list of Results.File instances.
maruel@chromium.org3683afe2013-07-27 00:09:27 +00003161 - blacklist: lambda to reject unneeded files, for example r'.+\.pyc'.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003162 """
3163 logging.info(
3164 'extract_directories(%s, %d files, ...)' % (root_dir, len(files)))
3165 assert not (root_dir or '').endswith(os.path.sep), root_dir
maruel@chromium.org306e0e72012-11-02 18:22:03 +00003166 # It is important for root_dir to not be a symlinked path, make sure to call
3167 # os.path.realpath() as needed.
3168 assert not root_dir or (
maruel@chromium.org561d4b22013-09-26 21:08:08 +00003169 os.path.realpath(file_path.get_native_path_case(root_dir)) == root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003170 assert not any(isinstance(f, Results.Directory) for f in files)
3171 # Remove non existent files.
3172 files = [f for f in files if f.existent]
3173 if not files:
3174 return files
3175 # All files must share the same root, which can be None.
3176 assert len(set(f.root for f in files)) == 1, set(f.root for f in files)
3177
3178 # Creates a {directory: {filename: File}} mapping, up to root.
3179 buckets = {}
3180 if root_dir:
3181 buckets[root_dir] = {}
3182 for fileobj in files:
3183 path = fileobj.full_path
3184 directory = os.path.dirname(path)
3185 assert directory
3186 # Do not use os.path.basename() so trailing os.path.sep is kept.
3187 basename = path[len(directory)+1:]
3188 files_in_directory = buckets.setdefault(directory, {})
3189 files_in_directory[basename] = fileobj
3190 # Add all the directories recursively up to root.
3191 while True:
3192 old_d = directory
3193 directory = os.path.dirname(directory)
3194 if directory + os.path.sep == root_dir or directory == old_d:
3195 break
3196 buckets.setdefault(directory, {})
3197
3198 root_prefix = len(root_dir) + 1 if root_dir else 0
3199 for directory in sorted(buckets, reverse=True):
maruel@chromium.orgb1678e62013-03-19 15:55:17 +00003200 if not os.path.isdir(directory):
3201 logging.debug(
3202 '%s was a directory but doesn\'t exist anymore; ignoring', directory)
3203 continue
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003204 actual = set(f for f in os.listdir(directory) if not blacklist(f))
3205 expected = set(buckets[directory])
3206 if not (actual - expected):
3207 parent = os.path.dirname(directory)
3208 buckets[parent][os.path.basename(directory)] = Results.Directory(
3209 root_dir,
3210 directory[root_prefix:],
3211 False,
3212 sum(f.size for f in buckets[directory].itervalues()),
3213 sum(f.nb_files for f in buckets[directory].itervalues()))
3214 # Remove the whole bucket.
3215 del buckets[directory]
3216
3217 # Reverse the mapping with what remains. The original instances are returned,
3218 # so the cached meta data is kept.
3219 files = sum((x.values() for x in buckets.itervalues()), [])
3220 return sorted(files, key=lambda x: x.path)
3221
3222
3223def trace(logfile, cmd, cwd, api, output):
3224 """Traces an executable. Returns (returncode, output) from api.
3225
3226 Arguments:
3227 - logfile: file to write to.
3228 - cmd: command to run.
3229 - cwd: current directory to start the process in.
3230 - api: a tracing api instance.
3231 - output: if True, returns output, otherwise prints it at the console.
3232 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003233 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003234 api.clean_trace(logfile)
3235 with api.get_tracer(logfile) as tracer:
3236 return tracer.trace(cmd, cwd, 'default', output)
3237
3238
maruel@chromium.orge5322512013-08-19 20:17:57 +00003239def CMDclean(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003240 """Cleans up traces."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003241 options, args = parser.parse_args(args)
3242 api = get_api()
3243 api.clean_trace(options.log)
3244 return 0
3245
3246
maruel@chromium.orge5322512013-08-19 20:17:57 +00003247def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003248 """Traces an executable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003249 parser.allow_interspersed_args = False
3250 parser.add_option(
3251 '-q', '--quiet', action='store_true',
3252 help='Redirects traced executable output to /dev/null')
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003253 parser.add_option(
3254 '-s', '--sudo', action='store_true',
3255 help='Use sudo when shelling out the tracer tool (ignored on Windows)')
3256 parser.add_option(
3257 '-n', '--no-sudo', action='store_false',
3258 help='Don\'t use sudo')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003259 options, args = parser.parse_args(args)
3260
3261 if not args:
3262 parser.error('Please provide a command to run')
3263
Marc-Antoine Ruel5906fcd2013-11-08 21:02:24 -05003264 if not can_trace():
3265 parser.error('Please rerun this program with admin privileges')
3266
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003267 if not os.path.isabs(args[0]) and os.access(args[0], os.X_OK):
3268 args[0] = os.path.abspath(args[0])
3269
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003270 # options.sudo default value is None, which is to do whatever tracer defaults
3271 # do.
3272 api = get_api(use_sudo=options.sudo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003273 return trace(options.log, args, os.getcwd(), api, options.quiet)[0]
3274
3275
maruel@chromium.orge5322512013-08-19 20:17:57 +00003276def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003277 """Reads the logs and prints the result."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003278 parser.add_option(
3279 '-V', '--variable',
3280 nargs=2,
3281 action='append',
3282 dest='variables',
3283 metavar='VAR_NAME directory',
3284 default=[],
3285 help=('Variables to replace relative directories against. Example: '
3286 '"-v \'$HOME\' \'/home/%s\'" will replace all occurence of your '
3287 'home dir with $HOME') % getpass.getuser())
3288 parser.add_option(
3289 '--root-dir',
3290 help='Root directory to base everything off it. Anything outside of this '
3291 'this directory will not be reported')
3292 parser.add_option(
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003293 '--trace-name',
3294 help='Only reads one of the trace. Defaults to reading all traces')
3295 parser.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003296 '-j', '--json', action='store_true',
3297 help='Outputs raw result data as json')
3298 parser.add_option(
maruel@chromium.org3683afe2013-07-27 00:09:27 +00003299 '--trace-blacklist', action='append', default=[],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003300 help='List of regexp to use as blacklist filter')
3301 options, args = parser.parse_args(args)
3302
3303 if options.root_dir:
maruel@chromium.org561d4b22013-09-26 21:08:08 +00003304 options.root_dir = file_path.get_native_path_case(
maruel@chromium.org7dc17b32013-04-02 22:27:05 +00003305 unicode(os.path.abspath(options.root_dir)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003306
3307 variables = dict(options.variables)
3308 api = get_api()
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05003309 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003310 data = api.parse_log(options.log, blacklist, options.trace_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003311 # Process each trace.
3312 output_as_json = []
maruel@chromium.org9e56b592013-05-21 16:56:05 +00003313 try:
3314 for item in data:
3315 if 'exception' in item:
3316 # Do not abort the other traces.
3317 print >> sys.stderr, (
3318 'Trace %s: Got an exception: %s' % (
3319 item['trace'], item['exception'][1]))
3320 continue
3321 results = item['results']
3322 if options.root_dir:
3323 results = results.strip_root(options.root_dir)
3324
3325 if options.json:
3326 output_as_json.append(results.flatten())
3327 else:
3328 simplified = extract_directories(
3329 options.root_dir, results.files, blacklist)
3330 simplified = [f.replace_variables(variables) for f in simplified]
3331 if len(data) > 1:
3332 print('Trace: %s' % item['trace'])
3333 print('Total: %d' % len(results.files))
3334 print('Non existent: %d' % len(results.non_existent))
3335 for f in results.non_existent:
3336 print(' %s' % f.path)
3337 print(
3338 'Interesting: %d reduced to %d' % (
3339 len(results.existent), len(simplified)))
3340 for f in simplified:
3341 print(' %s' % f.path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003342
3343 if options.json:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05003344 tools.write_json(sys.stdout, output_as_json, False)
maruel@chromium.org9e56b592013-05-21 16:56:05 +00003345 except KeyboardInterrupt:
3346 return 1
3347 except IOError as e:
3348 if e.errno == errno.EPIPE:
3349 # Do not print a stack trace when the output is piped to less and the user
3350 # quits before the whole output was written.
3351 return 1
3352 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003353 return 0
3354
3355
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003356class OptionParserTraceInputs(tools.OptionParserWithLogging):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003357 """Adds automatic --log handling."""
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003358
3359 # Disable --log-file options since both --log and --log-file options are
3360 # confusing.
3361 # TODO(vadimsh): Rename --log-file or --log to something else.
3362 enable_log_file = False
3363
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003364 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003365 tools.OptionParserWithLogging.__init__(self, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003366 self.add_option(
3367 '-l', '--log', help='Log file to generate or read, required')
3368
3369 def parse_args(self, *args, **kwargs):
3370 """Makes sure the paths make sense.
3371
3372 On Windows, / and \ are often mixed together in a path.
3373 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003374 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003375 self, *args, **kwargs)
3376 if not options.log:
3377 self.error('Must supply a log file with -l')
3378 options.log = os.path.abspath(options.log)
3379 return options, args
3380
3381
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003382def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00003383 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003384 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00003385 return dispatcher.execute(
3386 OptionParserTraceInputs(version=__version__), argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003387 except TracingFailure, e:
3388 sys.stderr.write('\nError: ')
3389 sys.stderr.write(str(e))
3390 sys.stderr.write('\n')
3391 return 1
3392
3393
3394if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00003395 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003396 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00003397 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003398 sys.exit(main(sys.argv[1:]))