blob: 29f66db04bfe7b810804b5780f851755b25d2d7b [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
2# coding=utf-8
maruelea586f32016-04-05 11:11:33 -07003# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07004# Use of this source code is governed under the Apache License, Version 2.0
5# that 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
maruel12e30012015-10-09 11:55:35 -070042from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040043from utils import logging_utils
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000044from utils import tools
45
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000046## OS-specific imports
47
48if sys.platform == 'win32':
maruel@chromium.org561d4b22013-09-26 21:08:08 +000049 from ctypes.wintypes import byref, c_int, c_wchar_p
50 from ctypes.wintypes import windll # pylint: disable=E0611
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000051
52
maruel12e30012015-10-09 11:55:35 -070053__version__ = '0.2'
maruel@chromium.org3d671992013-08-20 00:38:27 +000054
55
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000056BASE_DIR = os.path.dirname(os.path.abspath(__file__))
57ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR))
58
59
60class TracingFailure(Exception):
61 """An exception occured during tracing."""
62 def __init__(self, description, pid, line_number, line, *args):
63 super(TracingFailure, self).__init__(
64 description, pid, line_number, line, *args)
65 self.description = description
66 self.pid = pid
67 self.line_number = line_number
68 self.line = line
69 self.extra = args
70
71 def __str__(self):
72 out = self.description
73 if self.pid:
74 out += '\npid: %d' % self.pid
75 if self.line_number:
76 out += '\nline: %d' % self.line_number
77 if self.line:
78 out += '\n%s' % self.line
79 if self.extra:
80 out += '\n' + ', '.join(map(str, filter(None, self.extra)))
81 return out
82
83
84## OS-specific functions
85
86if sys.platform == 'win32':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000087 def get_current_encoding():
88 """Returns the 'ANSI' code page associated to the process."""
89 return 'cp%d' % int(windll.kernel32.GetACP())
90
91
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000092 def CommandLineToArgvW(command_line):
93 """Splits a commandline into argv using CommandLineToArgvW()."""
94 # http://msdn.microsoft.com/library/windows/desktop/bb776391.aspx
95 size = c_int()
maruel@chromium.org306e0e72012-11-02 18:22:03 +000096 assert isinstance(command_line, unicode)
97 ptr = windll.shell32.CommandLineToArgvW(command_line, byref(size))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000098 try:
99 return [arg for arg in (c_wchar_p * size.value).from_address(ptr)]
100 finally:
101 windll.kernel32.LocalFree(ptr)
102
103
Marc-Antoine Ruel5906fcd2013-11-08 21:02:24 -0500104def can_trace():
105 """Returns True if the user is an administrator on Windows.
106
107 It is required for tracing to work.
108 """
109 if sys.platform != 'win32':
110 return True
111 return bool(windll.shell32.IsUserAnAdmin())
112
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000113
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000114def create_subprocess_thunk():
115 """Creates a small temporary script to start the child process.
116
117 This thunk doesn't block, its unique name is used to identify it as the
118 parent.
119 """
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -0400120 handle, name = tempfile.mkstemp(prefix=u'trace_inputs_thunk', suffix='.py')
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000121 try:
122 os.write(
123 handle,
124 (
125 'import subprocess, sys\n'
126 'sys.exit(subprocess.call(sys.argv[2:]))\n'
127 ))
128 finally:
129 os.close(handle)
130 return name
131
132
133def create_exec_thunk():
134 """Creates a small temporary script to start the child executable.
135
136 Reads from the file handle provided as the fisrt argument to block, then
137 execv() the command to be traced.
138 """
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -0400139 handle, name = tempfile.mkstemp(prefix=u'trace_inputs_thunk', suffix='.py')
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000140 try:
141 os.write(
142 handle,
143 (
144 'import os, sys\n'
145 'fd = int(sys.argv[1])\n'
146 # This will block until the controlling process writes a byte on the
147 # pipe. It will do so once the tracing tool, e.g. strace, is ready to
148 # trace.
149 'os.read(fd, 1)\n'
150 'os.close(fd)\n'
151 'os.execv(sys.argv[2], sys.argv[2:])\n'
152 ))
153 finally:
154 os.close(handle)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000155 return name
156
157
158def strace_process_quoted_arguments(text):
159 """Extracts quoted arguments on a string and return the arguments as a list.
160
161 Implemented as an automaton. Supports incomplete strings in the form
162 '"foo"...'.
163
164 Example:
165 With text = '"foo", "bar"', the function will return ['foo', 'bar']
166
167 TODO(maruel): Implement escaping.
168 """
169 # All the possible states of the DFA.
170 ( NEED_QUOTE, # Begining of a new arguments.
171 INSIDE_STRING, # Inside an argument.
172 ESCAPED, # Found a '\' inside a quote. Treat the next char as-is.
173 NEED_COMMA_OR_DOT, # Right after the closing quote of an argument. Could be
174 # a serie of 3 dots or a comma.
175 NEED_SPACE, # Right after a comma
176 NEED_DOT_2, # Found a dot, need a second one.
177 NEED_DOT_3, # Found second dot, need a third one.
178 NEED_COMMA, # Found third dot, need a comma.
179 ) = range(8)
180
181 state = NEED_QUOTE
182 out = []
183 for index, char in enumerate(text):
184 if char == '"':
185 if state == NEED_QUOTE:
186 state = INSIDE_STRING
187 # A new argument was found.
188 out.append('')
189 elif state == INSIDE_STRING:
190 # The argument is now closed.
191 state = NEED_COMMA_OR_DOT
192 elif state == ESCAPED:
193 out[-1] += char
194 state = INSIDE_STRING
195 else:
196 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000197 'Can\'t process char \'%s\' at column %d for: %r' % (
198 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000199 index,
200 text)
201 elif char == ',':
202 if state in (NEED_COMMA_OR_DOT, NEED_COMMA):
203 state = NEED_SPACE
204 elif state == INSIDE_STRING:
205 out[-1] += char
206 elif state == ESCAPED:
207 out[-1] += char
208 state = INSIDE_STRING
209 else:
210 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000211 'Can\'t process char \'%s\' at column %d for: %r' % (
212 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000213 index,
214 text)
215 elif char == ' ':
216 if state == NEED_SPACE:
217 state = NEED_QUOTE
218 elif state == INSIDE_STRING:
219 out[-1] += char
220 elif state == ESCAPED:
221 out[-1] += char
222 state = INSIDE_STRING
223 else:
224 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000225 'Can\'t process char \'%s\' at column %d for: %r' % (
226 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000227 index,
228 text)
229 elif char == '.':
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000230 if state in (NEED_QUOTE, NEED_COMMA_OR_DOT):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000231 # The string is incomplete, this mean the strace -s flag should be
232 # increased.
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000233 # For NEED_QUOTE, the input string would look like '"foo", ...'.
234 # For NEED_COMMA_OR_DOT, the input string would look like '"foo"...'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000235 state = NEED_DOT_2
236 elif state == NEED_DOT_2:
237 state = NEED_DOT_3
238 elif state == NEED_DOT_3:
239 state = NEED_COMMA
240 elif state == INSIDE_STRING:
241 out[-1] += char
242 elif state == ESCAPED:
243 out[-1] += char
244 state = INSIDE_STRING
245 else:
246 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000247 'Can\'t process char \'%s\' at column %d for: %r' % (
248 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000249 index,
250 text)
251 elif char == '\\':
252 if state == ESCAPED:
253 out[-1] += char
254 state = INSIDE_STRING
255 elif state == INSIDE_STRING:
256 state = ESCAPED
257 else:
258 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000259 'Can\'t process char \'%s\' at column %d for: %r' % (
260 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000261 index,
262 text)
263 else:
264 if state == INSIDE_STRING:
265 out[-1] += char
266 else:
267 raise ValueError(
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000268 'Can\'t process char \'%s\' at column %d for: %r' % (
269 char, index, text),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000270 index,
271 text)
272 if state not in (NEED_COMMA, NEED_COMMA_OR_DOT):
273 raise ValueError(
274 'String is incorrectly terminated: %r' % text,
275 text)
276 return out
277
278
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000279def assert_is_renderable(pseudo_string):
280 """Asserts the input is a valid object to be processed by render()."""
281 assert (
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000282 pseudo_string is None or
283 isinstance(pseudo_string, unicode) or
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000284 hasattr(pseudo_string, 'render')), repr(pseudo_string)
285
286
287def render(pseudo_string):
288 """Converts the pseudo-string to an unicode string."""
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000289 if pseudo_string is None or isinstance(pseudo_string, unicode):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000290 return pseudo_string
291 return pseudo_string.render()
292
293
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000294class Results(object):
295 """Results of a trace session."""
296
297 class _TouchedObject(object):
298 """Something, a file or a directory, that was accessed."""
299 def __init__(self, root, path, tainted, size, nb_files):
300 logging.debug(
301 '%s(%s, %s, %s, %s, %s)' %
302 (self.__class__.__name__, root, path, tainted, size, nb_files))
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000303 assert_is_renderable(root)
304 assert_is_renderable(path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000305 self.root = root
306 self.path = path
307 self.tainted = tainted
308 self.nb_files = nb_files
maruel@chromium.org538141b2013-06-03 20:57:17 +0000309 # Can be used as a cache or a default value, depending on context. In
310 # particular, once self.tainted is True, because the path was replaced
311 # with a variable, it is not possible to look up the file size.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000312 self._size = size
313 # These are cache only.
314 self._real_path = None
315
316 # Check internal consistency.
317 assert path, path
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000318 assert tainted or bool(root) != bool(file_path.isabs(path)), (root, path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000319 assert tainted or (
maruel12e30012015-10-09 11:55:35 -0700320 not fs.exists(self.full_path) or
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000321 (self.full_path == file_path.get_native_path_case(self.full_path))), (
322 tainted,
323 self.full_path,
324 file_path.get_native_path_case(self.full_path))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000325
326 @property
327 def existent(self):
328 return self.size != -1
329
330 @property
331 def full_path(self):
332 if self.root:
333 return os.path.join(self.root, self.path)
334 return self.path
335
336 @property
337 def real_path(self):
338 """Returns the path with symlinks resolved."""
339 if not self._real_path:
maruel12e30012015-10-09 11:55:35 -0700340 self._real_path = fs.path.realpath(self.full_path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000341 return self._real_path
342
343 @property
344 def size(self):
maruel@chromium.org538141b2013-06-03 20:57:17 +0000345 """File's size. -1 is not existent.
346
347 Once tainted, it is not possible the retrieve the file size anymore since
348 the path is composed of variables.
349 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000350 if self._size is None and not self.tainted:
351 try:
maruel12e30012015-10-09 11:55:35 -0700352 self._size = fs.stat(self.full_path).st_size
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000353 except OSError:
354 self._size = -1
355 return self._size
356
357 def flatten(self):
358 """Returns a dict representing this object.
359
360 A 'size' of 0 means the file was only touched and not read.
361 """
362 return {
363 'path': self.path,
364 'size': self.size,
365 }
366
367 def replace_variables(self, variables):
368 """Replaces the root of this File with one of the variables if it matches.
369
370 If a variable replacement occurs, the cloned object becomes tainted.
371 """
372 for variable, root_path in variables.iteritems():
373 if self.path.startswith(root_path):
374 return self._clone(
375 self.root, variable + self.path[len(root_path):], True)
376 # No need to clone, returns ourself.
377 return self
378
379 def strip_root(self, root):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000380 """Returns a clone of itself with 'root' stripped off.
381
382 Note that the file is kept if it is either accessible from a symlinked
383 path that was used to access the file or through the real path.
384 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000385 # Check internal consistency.
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000386 assert (
387 self.tainted or
388 (file_path.isabs(root) and root.endswith(os.path.sep))), root
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000389 if not self.full_path.startswith(root):
390 # Now try to resolve the symlinks to see if it can be reached this way.
391 # Only try *after* trying without resolving symlink.
392 if not self.real_path.startswith(root):
393 return None
394 path = self.real_path
395 else:
396 path = self.full_path
397 return self._clone(root, path[len(root):], self.tainted)
398
399 def _clone(self, new_root, new_path, tainted):
400 raise NotImplementedError(self.__class__.__name__)
401
402 class File(_TouchedObject):
403 """A file that was accessed. May not be present anymore.
404
405 If tainted is true, it means it is not a real path anymore as a variable
406 replacement occured.
407
maruel@chromium.org538141b2013-06-03 20:57:17 +0000408 |mode| can be one of None, TOUCHED, READ or WRITE.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000409 """
maruel@chromium.org538141b2013-06-03 20:57:17 +0000410 # Was probed for existence, and it is existent, but was never _opened_.
411 TOUCHED = 't'
412 # Opened for read only and guaranteed to not have been written to.
413 READ = 'r'
414 # Opened for write.
415 WRITE = 'w'
416
417 # They are listed in order of priority. E.g. if a file is traced as TOUCHED
418 # then as WRITE, only keep WRITE. None means no idea, which is a problem on
419 # Windows.
420 ACCEPTABLE_MODES = (None, TOUCHED, READ, WRITE)
421
422 def __init__(self, root, path, tainted, size, mode):
423 assert mode in self.ACCEPTABLE_MODES
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000424 super(Results.File, self).__init__(root, path, tainted, size, 1)
maruel@chromium.org538141b2013-06-03 20:57:17 +0000425 self.mode = mode
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000426
427 def _clone(self, new_root, new_path, tainted):
428 """Clones itself keeping meta-data."""
429 # Keep the self.size and self._real_path caches for performance reason. It
430 # is also important when the file becomes tainted (with a variable instead
431 # of the real path) since self.path is not an on-disk path anymore so
432 # out._size cannot be updated.
maruel@chromium.org538141b2013-06-03 20:57:17 +0000433 out = self.__class__(new_root, new_path, tainted, self.size, self.mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000434 out._real_path = self._real_path
435 return out
436
maruel@chromium.org538141b2013-06-03 20:57:17 +0000437 def flatten(self):
438 out = super(Results.File, self).flatten()
439 out['mode'] = self.mode
440 return out
441
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000442 class Directory(_TouchedObject):
maruel@chromium.org538141b2013-06-03 20:57:17 +0000443 """A directory of files. Must exist.
444
445 For a Directory instance, self.size is not a cache, it's an actual value
446 that is never modified and represents the total size of the files contained
447 in this directory. It is possible that the directory is empty so that
448 size==0; this happens if there's only an invalid symlink in it.
449 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000450 def __init__(self, root, path, tainted, size, nb_files):
451 """path='.' is a valid value and must be handled appropriately."""
452 assert not path.endswith(os.path.sep), path
453 super(Results.Directory, self).__init__(
454 root, path + os.path.sep, tainted, size, nb_files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000455
456 def flatten(self):
457 out = super(Results.Directory, self).flatten()
458 out['nb_files'] = self.nb_files
459 return out
460
461 def _clone(self, new_root, new_path, tainted):
462 """Clones itself keeping meta-data."""
463 out = self.__class__(
464 new_root,
465 new_path.rstrip(os.path.sep),
466 tainted,
467 self.size,
468 self.nb_files)
469 out._real_path = self._real_path
470 return out
471
472 class Process(object):
473 """A process that was traced.
474
475 Contains references to the files accessed by this process and its children.
476 """
477 def __init__(self, pid, files, executable, command, initial_cwd, children):
478 logging.debug('Process(%s, %d, ...)' % (pid, len(files)))
479 self.pid = pid
480 self.files = sorted(files, key=lambda x: x.path)
481 self.children = children
482 self.executable = executable
483 self.command = command
484 self.initial_cwd = initial_cwd
485
486 # Check internal consistency.
487 assert len(set(f.path for f in self.files)) == len(self.files), sorted(
488 f.path for f in self.files)
489 assert isinstance(self.children, list)
490 assert isinstance(self.files, list)
491
492 @property
493 def all(self):
494 for child in self.children:
495 for i in child.all:
496 yield i
497 yield self
498
499 def flatten(self):
500 return {
501 'children': [c.flatten() for c in self.children],
502 'command': self.command,
503 'executable': self.executable,
504 'files': [f.flatten() for f in self.files],
505 'initial_cwd': self.initial_cwd,
506 'pid': self.pid,
507 }
508
509 def strip_root(self, root):
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000510 assert file_path.isabs(root) and root.endswith(os.path.sep), root
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000511 # Loads the files after since they are constructed as objects.
512 out = self.__class__(
513 self.pid,
514 filter(None, (f.strip_root(root) for f in self.files)),
515 self.executable,
516 self.command,
517 self.initial_cwd,
518 [c.strip_root(root) for c in self.children])
519 logging.debug(
520 'strip_root(%s) %d -> %d' % (root, len(self.files), len(out.files)))
521 return out
522
523 def __init__(self, process):
524 self.process = process
525 # Cache.
526 self._files = None
527
528 def flatten(self):
529 return {
530 'root': self.process.flatten(),
531 }
532
533 @property
534 def files(self):
535 if self._files is None:
536 self._files = sorted(
537 sum((p.files for p in self.process.all), []),
538 key=lambda x: x.path)
539 return self._files
540
541 @property
542 def existent(self):
543 return [f for f in self.files if f.existent]
544
545 @property
546 def non_existent(self):
547 return [f for f in self.files if not f.existent]
548
549 def strip_root(self, root):
550 """Returns a clone with all the files outside the directory |root| removed
551 and converts all the path to be relative paths.
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000552
553 It keeps files accessible through the |root| directory or that have been
554 accessed through any symlink which points to the same directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000555 """
556 # Resolve any symlink
maruel12e30012015-10-09 11:55:35 -0700557 root = fs.realpath(root)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000558 root = (
559 file_path.get_native_path_case(root).rstrip(os.path.sep) + os.path.sep)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000560 logging.debug('strip_root(%s)' % root)
561 return Results(self.process.strip_root(root))
562
563
564class ApiBase(object):
565 """OS-agnostic API to trace a process and its children."""
566 class Context(object):
567 """Processes one log line at a time and keeps the list of traced processes.
568
569 The parsing is complicated by the fact that logs are traced out of order for
570 strace but in-order for dtrace and logman. In addition, on Windows it is
571 very frequent that processids are reused so a flat list cannot be used. But
572 at the same time, it is impossible to faithfully construct a graph when the
573 logs are processed out of order. So both a tree and a flat mapping are used,
574 the tree is the real process tree, while the flat mapping stores the last
575 valid process for the corresponding processid. For the strace case, the
576 tree's head is guessed at the last moment.
577 """
578 class Process(object):
579 """Keeps context for one traced child process.
580
581 Logs all the files this process touched. Ignores directories.
582 """
583 def __init__(self, blacklist, pid, initial_cwd):
584 # Check internal consistency.
585 assert isinstance(pid, int), repr(pid)
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000586 assert_is_renderable(initial_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000587 self.pid = pid
588 # children are Process instances.
589 self.children = []
590 self.initial_cwd = initial_cwd
591 self.cwd = None
maruel@chromium.org538141b2013-06-03 20:57:17 +0000592 self.files = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000593 self.executable = None
594 self.command = None
595 self._blacklist = blacklist
596
597 def to_results_process(self):
598 """Resolves file case sensitivity and or late-bound strings."""
599 # When resolving files, it's normal to get dupe because a file could be
600 # opened multiple times with different case. Resolve the deduplication
601 # here.
602 def fix_path(x):
603 """Returns the native file path case.
604
605 Converts late-bound strings.
606 """
607 if not x:
608 # Do not convert None instance to 'None'.
609 return x
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000610 x = render(x)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000611 if os.path.isabs(x):
612 # If the path is not absolute, which tends to happen occasionally on
613 # Windows, it is not possible to get the native path case so ignore
614 # that trace. It mostly happens for 'executable' value.
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000615 x = file_path.get_native_path_case(x)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000616 return x
617
maruel@chromium.org538141b2013-06-03 20:57:17 +0000618 def fix_and_blacklist_path(x, m):
619 """Receives a tuple (filepath, mode) and processes filepath."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000620 x = fix_path(x)
621 if not x:
622 return
623 # The blacklist needs to be reapplied, since path casing could
624 # influence blacklisting.
625 if self._blacklist(x):
626 return
maruel@chromium.org538141b2013-06-03 20:57:17 +0000627 # Filters out directories. Some may have passed through.
maruel12e30012015-10-09 11:55:35 -0700628 if fs.isdir(x):
maruel@chromium.org538141b2013-06-03 20:57:17 +0000629 return
630 return x, m
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631
maruel@chromium.org538141b2013-06-03 20:57:17 +0000632 # Renders all the files as strings, as some could be RelativePath
633 # instances. It is important to do it first since there could still be
634 # multiple entries with the same path but different modes.
635 rendered = (
636 fix_and_blacklist_path(f, m) for f, m in self.files.iteritems())
637 files = sorted(
638 (f for f in rendered if f),
639 key=lambda x: (x[0], Results.File.ACCEPTABLE_MODES.index(x[1])))
640 # Then converting into a dict will automatically clean up lesser
641 # important values.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000642 files = [
maruel@chromium.org538141b2013-06-03 20:57:17 +0000643 Results.File(None, f, False, None, m)
644 for f, m in dict(files).iteritems()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000645 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000646 return Results.Process(
647 self.pid,
648 files,
649 fix_path(self.executable),
650 self.command,
651 fix_path(self.initial_cwd),
652 [c.to_results_process() for c in self.children])
653
maruel@chromium.org538141b2013-06-03 20:57:17 +0000654 def add_file(self, filepath, mode):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000655 """Adds a file if it passes the blacklist."""
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000656 if self._blacklist(render(filepath)):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000657 return
maruel@chromium.org538141b2013-06-03 20:57:17 +0000658 logging.debug('add_file(%d, %s, %s)', self.pid, filepath, mode)
659 # Note that filepath and not render(filepath) is added. It is because
660 # filepath could be something else than a string, like a RelativePath
661 # instance for dtrace logs.
662 modes = Results.File.ACCEPTABLE_MODES
663 old_mode = self.files.setdefault(filepath, mode)
664 if old_mode != mode and modes.index(old_mode) < modes.index(mode):
665 # Take the highest value.
666 self.files[filepath] = mode
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000667
668 def __init__(self, blacklist):
669 self.blacklist = blacklist
670 # Initial process.
671 self.root_process = None
672 # dict to accelerate process lookup, to not have to lookup the whole graph
673 # each time.
674 self._process_lookup = {}
675
676 class Tracer(object):
677 """During it's lifetime, the tracing subsystem is enabled."""
678 def __init__(self, logname):
679 self._logname = logname
maruel@chromium.org808e3f22013-05-16 16:42:54 +0000680 self._lock = threading.RLock()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000681 self._traces = []
682 self._initialized = True
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +0000683 self._scripts_to_cleanup = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000684
685 def trace(self, cmd, cwd, tracename, output):
686 """Runs the OS-specific trace program on an executable.
687
688 Arguments:
689 - cmd: The command (a list) to run.
690 - cwd: Current directory to start the child process in.
691 - tracename: Name of the trace in the logname file.
692 - output: If False, redirects output to PIPEs.
693
694 Returns a tuple (resultcode, output) and updates the internal trace
695 entries.
696 """
697 # The implementation adds an item to self._traces.
698 raise NotImplementedError(self.__class__.__name__)
699
700 def close(self, _timeout=None):
701 """Saves the meta-data in the logname file.
702
703 For kernel-based tracing, stops the tracing subsystem.
704
705 Must not be used manually when using 'with' construct.
706 """
707 with self._lock:
maruel@chromium.org808e3f22013-05-16 16:42:54 +0000708 if not self._initialized:
709 raise TracingFailure(
710 'Called %s.close() on an unitialized object' %
711 self.__class__.__name__,
712 None, None, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000713 try:
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +0000714 while self._scripts_to_cleanup:
715 try:
maruel12e30012015-10-09 11:55:35 -0700716 fs.remove(self._scripts_to_cleanup.pop())
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +0000717 except OSError as e:
718 logging.error('Failed to delete a temporary script: %s', e)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500719 tools.write_json(self._logname, self._gen_logdata(), False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000720 finally:
721 self._initialized = False
722
723 def post_process_log(self):
724 """Post-processes the log so it becomes faster to load afterward.
725
726 Must not be used manually when using 'with' construct.
727 """
728 assert not self._initialized, 'Must stop tracing first.'
729
maruel@chromium.org808e3f22013-05-16 16:42:54 +0000730 def _gen_logdata(self):
731 """Returns the data to be saved in the trace file."""
732 return {
733 'traces': self._traces,
734 }
735
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000736 def __enter__(self):
737 """Enables 'with' statement."""
738 return self
739
740 def __exit__(self, exc_type, exc_value, traceback):
741 """Enables 'with' statement."""
742 self.close()
743 # If an exception was thrown, do not process logs.
744 if not exc_type:
745 self.post_process_log()
746
747 def get_tracer(self, logname):
748 """Returns an ApiBase.Tracer instance.
749
750 Initializes the tracing subsystem, which is a requirement for kernel-based
751 tracers. Only one tracer instance should be live at a time!
752
753 logname is the filepath to the json file that will contain the meta-data
754 about the logs.
755 """
756 return self.Tracer(logname)
757
758 @staticmethod
759 def clean_trace(logname):
760 """Deletes an old log."""
761 raise NotImplementedError()
762
763 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +0000764 def parse_log(cls, logname, blacklist, trace_name):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000765 """Processes trace logs and returns the files opened and the files that do
766 not exist.
767
768 It does not track directories.
769
maruel@chromium.orgec74ff82012-10-29 18:14:47 +0000770 Arguments:
771 - logname: must be an absolute path.
772 - blacklist: must be a lambda.
773 - trace_name: optional trace to read, defaults to reading all traces.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000774
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000775 Most of the time, files that do not exist are temporary test files that
776 should be put in /tmp instead. See http://crbug.com/116251.
777
778 Returns a list of dict with keys:
779 - results: A Results instance.
780 - trace: The corresponding tracename parameter provided to
781 get_tracer().trace().
782 - output: Output gathered during execution, if get_tracer().trace(...,
783 output=False) was used.
784 """
785 raise NotImplementedError(cls.__class__.__name__)
786
787
788class Strace(ApiBase):
789 """strace implies linux."""
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000790 @staticmethod
791 def load_filename(filename):
792 """Parses a filename in a log."""
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000793 # TODO(maruel): Be compatible with strace -x.
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000794 assert isinstance(filename, str)
795 out = ''
796 i = 0
797 while i < len(filename):
798 c = filename[i]
799 if c == '\\':
800 out += chr(int(filename[i+1:i+4], 8))
801 i += 4
802 else:
803 out += c
804 i += 1
805 # TODO(maruel): That's not necessarily true that the current code page is
806 # utf-8.
807 return out.decode('utf-8')
808
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000809 class Context(ApiBase.Context):
810 """Processes a strace log line and keeps the list of existent and non
811 existent files accessed.
812
813 Ignores directories.
814
815 Uses late-binding to processes the cwd of each process. The problem is that
816 strace generates one log file per process it traced but doesn't give any
maruel@chromium.org2909b7c2013-05-29 18:52:59 +0000817 information about which process was started when and by who. So process the
818 logs out of order and use late binding with RelativePath to be able to
819 deduce the initial directory of each process once all the logs are parsed.
820
821 TODO(maruel): Use the script even in the non-sudo case, so log parsing can
822 be done in two phase: first find the root process, then process the child
823 processes in order. With that, it should be possible to not use RelativePath
824 anymore. This would significantly simplify the code!
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000825 """
826 class Process(ApiBase.Context.Process):
827 """Represents the state of a process.
828
829 Contains all the information retrieved from the pid-specific log.
830 """
831 # Function names are using ([a-z_0-9]+)
832 # This is the most common format. function(args) = result
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000833 RE_HEADER = re.compile(r'^([a-z_0-9]+)\((.*?)\)\s+= (.+)$')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000834 # An interrupted function call, only grab the minimal header.
835 RE_UNFINISHED = re.compile(r'^([^\(]+)(.*) \<unfinished \.\.\.\>$')
836 # A resumed function call.
837 RE_RESUMED = re.compile(r'^<\.\.\. ([^ ]+) resumed> (.+)$')
838 # A process received a signal.
839 RE_SIGNAL = re.compile(r'^--- SIG[A-Z]+ .+ ---')
840 # A process didn't handle a signal. Ignore any junk appearing before,
841 # because the process was forcibly killed so it won't open any new file.
842 RE_KILLED = re.compile(
843 r'^.*\+\+\+ killed by ([A-Z]+)( \(core dumped\))? \+\+\+$')
844 # The process has exited.
845 RE_PROCESS_EXITED = re.compile(r'^\+\+\+ exited with (\d+) \+\+\+')
846 # A call was canceled. Ignore any prefix.
847 RE_UNAVAILABLE = re.compile(r'^.*\)\s*= \? <unavailable>$')
848 # Happens when strace fails to even get the function name.
849 UNNAMED_FUNCTION = '????'
850
851 # Corner-case in python, a class member function decorator must not be
852 # @staticmethod.
853 def parse_args(regexp, expect_zero): # pylint: disable=E0213
854 """Automatically convert the str 'args' into a list of processed
855 arguments.
856
857 Arguments:
858 - regexp is used to parse args.
859 - expect_zero: one of True, False or None.
860 - True: will check for result.startswith('0') first and will ignore
861 the trace line completely otherwise. This is important because for
862 many functions, the regexp will not process if the call failed.
863 - False: will check for not result.startswith(('?', '-1')) for the
864 same reason than with True.
865 - None: ignore result.
866 """
867 def meta_hook(function):
868 assert function.__name__.startswith('handle_')
869 def hook(self, args, result):
870 if expect_zero is True and not result.startswith('0'):
871 return
872 if expect_zero is False and result.startswith(('?', '-1')):
873 return
874 match = re.match(regexp, args)
875 if not match:
876 raise TracingFailure(
877 'Failed to parse %s(%s) = %s' %
878 (function.__name__[len('handle_'):], args, result),
879 None, None, None)
880 return function(self, match.groups(), result)
881 return hook
882 return meta_hook
883
884 class RelativePath(object):
885 """A late-bound relative path."""
886 def __init__(self, parent, value):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000887 assert_is_renderable(parent)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000888 self.parent = parent
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000889 assert (
890 value is None or
891 (isinstance(value, unicode) and not os.path.isabs(value)))
892 self.value = value
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000893 if self.value:
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000894 # TODO(maruel): On POSIX, '\\' is a valid character so remove this
895 # assert.
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000896 assert '\\' not in self.value, value
897 assert '\\' not in self.value, (repr(value), repr(self.value))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000898
899 def render(self):
900 """Returns the current directory this instance is representing.
901
902 This function is used to return the late-bound value.
903 """
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000904 assert self.parent is not None
905 parent = render(self.parent)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000906 if self.value:
907 return os.path.normpath(os.path.join(parent, self.value))
908 return parent
909
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000910 def __init__(self, root, pid):
911 """Keeps enough information to be able to guess the original process
912 root.
913
914 strace doesn't store which process was the initial process. So more
915 information needs to be kept so the graph can be reconstructed from the
916 flat map.
917 """
918 logging.info('%s(%d)' % (self.__class__.__name__, pid))
919 super(Strace.Context.Process, self).__init__(root.blacklist, pid, None)
920 assert isinstance(root, ApiBase.Context)
921 self._root = weakref.ref(root)
922 # The dict key is the function name of the pending call, like 'open'
923 # or 'execve'.
924 self._pending_calls = {}
925 self._line_number = 0
926 # Current directory when the process started.
maruel@chromium.orgbea76be2013-06-03 20:59:25 +0000927 if isinstance(self._root(), unicode):
928 self.initial_cwd = self._root()
929 else:
930 self.initial_cwd = self.RelativePath(self._root(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000931 self.parentid = None
maruel@chromium.org9f72e0f2012-10-18 14:48:49 +0000932 self._done = False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000933
934 def get_cwd(self):
935 """Returns the best known value of cwd."""
936 return self.cwd or self.initial_cwd
937
938 def render(self):
939 """Returns the string value of the RelativePath() object.
940
941 Used by RelativePath. Returns the initial directory and not the
942 current one since the current directory 'cwd' validity is time-limited.
943
944 The validity is only guaranteed once all the logs are processed.
945 """
946 return self.initial_cwd.render()
947
948 def on_line(self, line):
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000949 assert isinstance(line, str)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000950 self._line_number += 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000951 try:
maruel@chromium.org588e09d2013-05-21 14:38:57 +0000952 if self._done:
953 raise TracingFailure(
954 'Found a trace for a terminated process or corrupted log',
955 None, None, None)
956
957 if self.RE_SIGNAL.match(line):
958 # Ignore signals.
959 return
960
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000961 match = self.RE_KILLED.match(line)
962 if match:
963 # Converts a '+++ killed by Foo +++' trace into an exit_group().
964 self.handle_exit_group(match.group(1), None)
965 return
966
967 match = self.RE_PROCESS_EXITED.match(line)
968 if match:
969 # Converts a '+++ exited with 1 +++' trace into an exit_group()
970 self.handle_exit_group(match.group(1), None)
971 return
972
973 match = self.RE_UNFINISHED.match(line)
974 if match:
975 if match.group(1) in self._pending_calls:
976 raise TracingFailure(
977 'Found two unfinished calls for the same function',
978 None, None, None,
979 self._pending_calls)
980 self._pending_calls[match.group(1)] = (
981 match.group(1) + match.group(2))
982 return
983
984 match = self.RE_UNAVAILABLE.match(line)
985 if match:
986 # This usually means a process was killed and a pending call was
987 # canceled.
988 # TODO(maruel): Look up the last exit_group() trace just above and
989 # make sure any self._pending_calls[anything] is properly flushed.
990 return
991
992 match = self.RE_RESUMED.match(line)
993 if match:
994 if match.group(1) not in self._pending_calls:
995 raise TracingFailure(
996 'Found a resumed call that was not logged as unfinished',
997 None, None, None,
998 self._pending_calls)
999 pending = self._pending_calls.pop(match.group(1))
1000 # Reconstruct the line.
1001 line = pending + match.group(2)
1002
1003 match = self.RE_HEADER.match(line)
1004 if not match:
maruel@chromium.orgbcfc9e12013-03-19 18:47:17 +00001005 # The line is corrupted. It happens occasionally when a process is
1006 # killed forcibly with activity going on. Assume the process died.
1007 # No other line can be processed afterward.
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001008 logging.debug('%d is done: %s', self.pid, line)
maruel@chromium.orgbcfc9e12013-03-19 18:47:17 +00001009 self._done = True
1010 return
1011
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001012 if match.group(1) == self.UNNAMED_FUNCTION:
1013 return
1014
1015 # It's a valid line, handle it.
1016 handler = getattr(self, 'handle_%s' % match.group(1), None)
1017 if not handler:
1018 self._handle_unknown(match.group(1), match.group(2), match.group(3))
1019 return handler(match.group(2), match.group(3))
1020 except TracingFailure, e:
1021 # Hack in the values since the handler could be a static function.
1022 e.pid = self.pid
1023 e.line = line
1024 e.line_number = self._line_number
1025 # Re-raise the modified exception.
1026 raise
1027 except (KeyError, NotImplementedError, ValueError), e:
1028 raise TracingFailure(
1029 'Trace generated a %s exception: %s' % (
1030 e.__class__.__name__, str(e)),
1031 self.pid,
1032 self._line_number,
1033 line,
1034 e)
1035
1036 @parse_args(r'^\"(.+?)\", [FKORWX_|]+$', True)
1037 def handle_access(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001038 self._handle_file(args[0], Results.File.TOUCHED)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001039
1040 @parse_args(r'^\"(.+?)\"$', True)
1041 def handle_chdir(self, args, _result):
1042 """Updates cwd."""
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001043 self.cwd = self._mangle(args[0])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001044 logging.debug('handle_chdir(%d, %s)' % (self.pid, self.cwd))
1045
maruel@chromium.org538141b2013-06-03 20:57:17 +00001046 @parse_args(r'^\"(.+?)\", (\d+), (\d+)$', False)
1047 def handle_chown(self, args, _result):
1048 # TODO(maruel): Look at result?
1049 self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org55550722013-05-28 14:43:14 +00001050
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001051 def handle_clone(self, _args, result):
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001052 self._handling_forking('clone', result)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001053
1054 def handle_close(self, _args, _result):
1055 pass
1056
maruel@chromium.org538141b2013-06-03 20:57:17 +00001057 @parse_args(r'^\"(.+?)\", (\d+)$', False)
1058 def handle_chmod(self, args, _result):
1059 self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001060
maruel@chromium.org538141b2013-06-03 20:57:17 +00001061 @parse_args(r'^\"(.+?)\", (\d+)$', False)
1062 def handle_creat(self, args, _result):
1063 self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001064
1065 @parse_args(r'^\"(.+?)\", \[(.+)\], \[\/\* \d+ vars? \*\/\]$', True)
1066 def handle_execve(self, args, _result):
1067 # Even if in practice execve() doesn't returns when it succeeds, strace
1068 # still prints '0' as the result.
1069 filepath = args[0]
maruel@chromium.org538141b2013-06-03 20:57:17 +00001070 self._handle_file(filepath, Results.File.READ)
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001071 self.executable = self._mangle(filepath)
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001072 try:
1073 self.command = strace_process_quoted_arguments(args[1])
1074 except ValueError as e:
1075 raise TracingFailure(
1076 'Failed to process command line argument:\n%s' % e.args[0],
1077 None, None, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001078
1079 def handle_exit_group(self, _args, _result):
1080 """Removes cwd."""
1081 self.cwd = None
1082
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001083 @parse_args(r'^(\d+|AT_FDCWD), \"(.*?)\", ([A-Z\_\|]+)(|, \d+)$', True)
1084 def handle_faccessat(self, args, _results):
1085 if args[0] == 'AT_FDCWD':
maruel@chromium.org538141b2013-06-03 20:57:17 +00001086 self._handle_file(args[1], Results.File.TOUCHED)
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001087 else:
1088 raise Exception('Relative faccess not implemented.')
1089
maruel@chromium.org0781f322013-05-28 19:45:49 +00001090 def handle_fallocate(self, _args, result):
1091 pass
1092
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001093 def handle_fork(self, args, result):
1094 self._handle_unknown('fork', args, result)
1095
maruel@chromium.org9f72e0f2012-10-18 14:48:49 +00001096 def handle_futex(self, _args, _result):
1097 pass
1098
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001099 @parse_args(r'^(\".+?\"|0x[a-f0-9]+), (\d+)$', False)
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001100 def handle_getcwd(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001101 # TODO(maruel): Resolve file handle.
1102 if not args[0].startswith('0x'):
1103 filepath = args[0][1:-1]
1104 if os.path.isabs(filepath):
1105 logging.debug('handle_chdir(%d, %s)' % (self.pid, self.cwd))
1106 if not isinstance(self.cwd, unicode):
1107 # Take the occasion to reset the path.
1108 self.cwd = self._mangle(filepath)
1109 else:
1110 # It should always match.
1111 assert self.cwd == Strace.load_filename(filepath), (
1112 self.cwd, filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001113
1114 @parse_args(r'^\"(.+?)\", \"(.+?)\"$', True)
1115 def handle_link(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001116 self._handle_file(args[0], Results.File.READ)
1117 self._handle_file(args[1], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001118
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001119 @parse_args(r'^(\".+?\"|0x[a-f0-9]+), \{(?:.+?, )?\.\.\.\}$', True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001120 def handle_lstat(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001121 # TODO(maruel): Resolve file handle.
1122 if not args[0].startswith('0x'):
1123 self._handle_file(args[0][1:-1], Results.File.TOUCHED)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001124
1125 def handle_mkdir(self, _args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001126 # We track content, not directories.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001127 pass
1128
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001129 @parse_args(r'^(\".*?\"|0x[a-f0-9]+), ([A-Z\_\|]+)(|, \d+)$', False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001130 def handle_open(self, args, _result):
1131 if 'O_DIRECTORY' in args[1]:
1132 return
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001133 # TODO(maruel): Resolve file handle.
1134 if not args[0].startswith('0x'):
1135 t = Results.File.READ if 'O_RDONLY' in args[1] else Results.File.WRITE
1136 self._handle_file(args[0][1:-1], t)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001137
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001138 @parse_args(
1139 r'^(\d+|AT_FDCWD), (\".*?\"|0x[a-f0-9]+), ([A-Z\_\|]+)(|, \d+)$',
1140 False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001141 def handle_openat(self, args, _result):
1142 if 'O_DIRECTORY' in args[2]:
1143 return
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001144 if args[0] == 'AT_FDCWD':
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001145 # TODO(maruel): Resolve file handle.
1146 if not args[1].startswith('0x'):
1147 t = (
1148 Results.File.READ if 'O_RDONLY' in args[2]
maruel@chromium.org538141b2013-06-03 20:57:17 +00001149 else Results.File.WRITE)
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001150 self._handle_file(args[1][1:-1], t)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001151 else:
1152 # TODO(maruel): Implement relative open if necessary instead of the
1153 # AT_FDCWD flag, let's hope not since this means tracking all active
1154 # directory handles.
maruel@chromium.org538141b2013-06-03 20:57:17 +00001155 raise NotImplementedError('Relative open via openat not implemented.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001156
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001157 @parse_args(
1158 r'^(\".+?\"|0x[a-f0-9]+), (?:\".+?\"(?:\.\.\.)?|0x[a-f0-9]+), '
1159 '\d+$',
1160 False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001161 def handle_readlink(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001162 # TODO(maruel): Resolve file handle.
1163 if not args[0].startswith('0x'):
1164 self._handle_file(args[0][1:-1], Results.File.READ)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001165
1166 @parse_args(r'^\"(.+?)\", \"(.+?)\"$', True)
1167 def handle_rename(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001168 self._handle_file(args[0], Results.File.READ)
1169 self._handle_file(args[1], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001170
1171 def handle_rmdir(self, _args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001172 # We track content, not directories.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001173 pass
1174
1175 def handle_setxattr(self, _args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001176 # TODO(maruel): self._handle_file(args[0], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001177 pass
1178
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001179 @parse_args(r'^(\".+?\"|0x[a-f0-9]+), \{.+?\}$', True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001180 def handle_stat(self, args, _result):
Marc-Antoine Ruelaa4fff52014-10-24 09:10:13 -04001181 # TODO(maruel): Resolve file handle.
1182 if not args[0].startswith('0x'):
1183 self._handle_file(args[0][1:-1], Results.File.TOUCHED)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001184
maruel@chromium.org538141b2013-06-03 20:57:17 +00001185 @parse_args(r'^\"(.+?)\", \"(.+?)\"$', True)
1186 def handle_symlink(self, args, _result):
1187 self._handle_file(args[0], Results.File.TOUCHED)
1188 self._handle_file(args[1], Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001189
csharp@chromium.orgfa672922012-11-02 17:43:31 +00001190 @parse_args(r'^\"(.+?)\", \d+', True)
1191 def handle_truncate(self, args, _result):
maruel@chromium.org538141b2013-06-03 20:57:17 +00001192 self._handle_file(args[0], Results.File.WRITE)
csharp@chromium.orgfa672922012-11-02 17:43:31 +00001193
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001194 def handle_unlink(self, _args, _result):
1195 # In theory, the file had to be created anyway.
1196 pass
1197
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001198 def handle_unlinkat(self, _args, _result):
1199 # In theory, the file had to be created anyway.
1200 pass
1201
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001202 def handle_statfs(self, _args, _result):
1203 pass
1204
csharp@chromium.org11ae0672012-10-30 14:53:17 +00001205 def handle_utimensat(self, _args, _result):
1206 pass
1207
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001208 def handle_vfork(self, _args, result):
1209 self._handling_forking('vfork', result)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001210
1211 @staticmethod
1212 def _handle_unknown(function, args, result):
1213 raise TracingFailure(
1214 'Unexpected/unimplemented trace %s(%s)= %s' %
1215 (function, args, result),
1216 None, None, None)
1217
maruel@chromium.org588e09d2013-05-21 14:38:57 +00001218 def _handling_forking(self, name, result):
1219 """Transfers cwd."""
1220 if result.startswith(('?', '-1')):
1221 # The call failed.
1222 return
1223 # Update the other process right away.
1224 childpid = int(result)
1225 child = self._root().get_or_set_proc(childpid)
1226 if child.parentid is not None or childpid in self.children:
1227 raise TracingFailure(
1228 'Found internal inconsitency in process lifetime detection '
1229 'during a %s() call' % name,
1230 None, None, None)
1231
1232 # Copy the cwd object.
1233 child.initial_cwd = self.get_cwd()
1234 child.parentid = self.pid
1235 # It is necessary because the logs are processed out of order.
1236 self.children.append(child)
1237
maruel@chromium.org538141b2013-06-03 20:57:17 +00001238 def _handle_file(self, filepath, mode):
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001239 filepath = self._mangle(filepath)
maruel@chromium.org538141b2013-06-03 20:57:17 +00001240 self.add_file(filepath, mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001241
maruel@chromium.orgbea76be2013-06-03 20:59:25 +00001242 def _mangle(self, filepath):
1243 """Decodes a filepath found in the log and convert it to a late-bound
1244 path if necessary.
1245
1246 |filepath| is an strace 'encoded' string and the returned value is
1247 either an unicode string if the path was absolute or a late bound path
1248 otherwise.
1249 """
1250 filepath = Strace.load_filename(filepath)
1251 if os.path.isabs(filepath):
1252 return filepath
1253 else:
1254 if isinstance(self.get_cwd(), unicode):
1255 return os.path.normpath(os.path.join(self.get_cwd(), filepath))
1256 return self.RelativePath(self.get_cwd(), filepath)
1257
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001258 def __init__(self, blacklist, root_pid, initial_cwd):
1259 """|root_pid| may be None when the root process is not known.
1260
1261 In that case, a search is done after reading all the logs to figure out
1262 the root process.
1263 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001264 super(Strace.Context, self).__init__(blacklist)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001265 assert_is_renderable(initial_cwd)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001266 self.root_pid = root_pid
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001267 self.initial_cwd = initial_cwd
1268
1269 def render(self):
1270 """Returns the string value of the initial cwd of the root process.
1271
1272 Used by RelativePath.
1273 """
1274 return self.initial_cwd
1275
1276 def on_line(self, pid, line):
1277 """Transfers control into the Process.on_line() function."""
1278 self.get_or_set_proc(pid).on_line(line.strip())
1279
1280 def to_results(self):
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001281 """If necessary, finds back the root process and verify consistency."""
1282 if not self.root_pid:
1283 # The non-sudo case. The traced process was started by strace itself,
1284 # so the pid of the traced process is not known.
1285 root = [p for p in self._process_lookup.itervalues() if not p.parentid]
1286 if len(root) == 1:
1287 self.root_process = root[0]
1288 # Save it for later.
1289 self.root_pid = self.root_process.pid
1290 else:
1291 # The sudo case. The traced process was started manually so its pid is
1292 # known.
1293 self.root_process = self._process_lookup.get(self.root_pid)
1294 if not self.root_process:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001295 raise TracingFailure(
1296 'Found internal inconsitency in process lifetime detection '
1297 'while finding the root process',
1298 None,
1299 None,
1300 None,
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001301 self.root_pid,
1302 sorted(self._process_lookup))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001303 process = self.root_process.to_results_process()
1304 if sorted(self._process_lookup) != sorted(p.pid for p in process.all):
1305 raise TracingFailure(
1306 'Found internal inconsitency in process lifetime detection '
1307 'while looking for len(tree) == len(list)',
1308 None,
1309 None,
1310 None,
1311 sorted(self._process_lookup),
1312 sorted(p.pid for p in process.all))
1313 return Results(process)
1314
1315 def get_or_set_proc(self, pid):
1316 """Returns the Context.Process instance for this pid or creates a new one.
1317 """
1318 if not pid or not isinstance(pid, int):
1319 raise TracingFailure(
1320 'Unpexpected value for pid: %r' % pid,
1321 pid,
1322 None,
1323 None,
1324 pid)
1325 if pid not in self._process_lookup:
1326 self._process_lookup[pid] = self.Process(self, pid)
1327 return self._process_lookup[pid]
1328
1329 @classmethod
1330 def traces(cls):
1331 """Returns the list of all handled traces to pass this as an argument to
1332 strace.
1333 """
1334 prefix = 'handle_'
1335 return [i[len(prefix):] for i in dir(cls.Process) if i.startswith(prefix)]
1336
1337 class Tracer(ApiBase.Tracer):
1338 MAX_LEN = 256
1339
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00001340 def __init__(self, logname, use_sudo):
1341 super(Strace.Tracer, self).__init__(logname)
1342 self.use_sudo = use_sudo
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001343 if use_sudo:
1344 # TODO(maruel): Use the jump script systematically to make it easy to
1345 # figure out the root process, so RelativePath is not necessary anymore.
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00001346 self._child_script = create_exec_thunk()
1347 self._scripts_to_cleanup.append(self._child_script)
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00001348
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001349 def trace(self, cmd, cwd, tracename, output):
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001350 """Runs strace on an executable.
1351
1352 When use_sudo=True, it is a 3-phases process: start the thunk, start
1353 sudo strace with the pid of the thunk and then have the thunk os.execve()
1354 the process to trace.
1355 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001356 logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
1357 assert os.path.isabs(cmd[0]), cmd[0]
1358 assert os.path.isabs(cwd), cwd
1359 assert os.path.normpath(cwd) == cwd, cwd
1360 with self._lock:
1361 if not self._initialized:
1362 raise TracingFailure(
1363 'Called Tracer.trace() on an unitialized object',
1364 None, None, None, tracename)
1365 assert tracename not in (i['trace'] for i in self._traces)
1366 stdout = stderr = None
1367 if output:
1368 stdout = subprocess.PIPE
1369 stderr = subprocess.STDOUT
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001370
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001371 # Ensure all file related APIs are hooked.
1372 traces = ','.join(Strace.Context.traces() + ['file'])
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001373 flags = [
maruel@chromium.orga5cfdc32013-05-28 14:46:15 +00001374 # Each child process has its own trace file. It is necessary because
1375 # strace may generate corrupted log file if multiple processes are
1376 # heavily doing syscalls simultaneously.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001377 '-ff',
maruel@chromium.orga5cfdc32013-05-28 14:46:15 +00001378 # Reduce whitespace usage.
1379 '-a1',
1380 # hex encode non-ascii strings.
1381 # TODO(maruel): '-x',
1382 # TODO(maruel): '-ttt',
1383 # Signals are unnecessary noise here. Note the parser can cope with them
1384 # but reduce the unnecessary output.
1385 '-esignal=none',
1386 # Print as much data as wanted.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001387 '-s', '%d' % self.MAX_LEN,
1388 '-e', 'trace=%s' % traces,
1389 '-o', self._logname + '.' + tracename,
1390 ]
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001391
1392 if self.use_sudo:
1393 pipe_r, pipe_w = os.pipe()
1394 # Start the child process paused.
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00001395 target_cmd = [sys.executable, self._child_script, str(pipe_r)] + cmd
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001396 logging.debug(' '.join(target_cmd))
1397 child_proc = subprocess.Popen(
1398 target_cmd,
1399 stdin=subprocess.PIPE,
1400 stdout=stdout,
1401 stderr=stderr,
1402 cwd=cwd)
1403
1404 # TODO(maruel): both processes must use the same UID for it to work
1405 # without sudo. Look why -p is failing at the moment without sudo.
1406 trace_cmd = [
1407 'sudo',
1408 'strace',
1409 '-p', str(child_proc.pid),
1410 ] + flags
1411 logging.debug(' '.join(trace_cmd))
1412 strace_proc = subprocess.Popen(
1413 trace_cmd,
1414 cwd=cwd,
1415 stdin=subprocess.PIPE,
1416 stdout=subprocess.PIPE,
1417 stderr=subprocess.PIPE)
1418
1419 line = strace_proc.stderr.readline()
1420 if not re.match(r'^Process \d+ attached \- interrupt to quit$', line):
1421 # TODO(maruel): Raise an exception.
1422 assert False, line
1423
1424 # Now fire the child process.
1425 os.write(pipe_w, 'x')
1426
1427 out = child_proc.communicate()[0]
1428 strace_out = strace_proc.communicate()[0]
1429
1430 # TODO(maruel): if strace_proc.returncode: Add an exception.
1431 saved_out = strace_out if strace_proc.returncode else out
1432 root_pid = child_proc.pid
1433 else:
1434 # Non-sudo case.
1435 trace_cmd = [
1436 'strace',
1437 ] + flags + cmd
1438 logging.debug(' '.join(trace_cmd))
1439 child_proc = subprocess.Popen(
1440 trace_cmd,
1441 cwd=cwd,
1442 stdin=subprocess.PIPE,
1443 stdout=stdout,
1444 stderr=stderr)
1445 out = child_proc.communicate()[0]
1446 # TODO(maruel): Walk the logs and figure out the root process would
1447 # simplify parsing the logs a *lot*.
1448 saved_out = out
1449 # The trace reader will have to figure out.
1450 root_pid = None
1451
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001452 with self._lock:
1453 assert tracename not in (i['trace'] for i in self._traces)
1454 self._traces.append(
1455 {
1456 'cmd': cmd,
1457 'cwd': cwd,
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001458 'output': saved_out,
1459 'pid': root_pid,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001460 'trace': tracename,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001461 })
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001462 return child_proc.returncode, out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001463
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00001464 def __init__(self, use_sudo=None):
1465 super(Strace, self).__init__()
1466 self.use_sudo = use_sudo
1467
1468 def get_tracer(self, logname):
1469 return self.Tracer(logname, self.use_sudo)
1470
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001471 @staticmethod
1472 def clean_trace(logname):
maruel12e30012015-10-09 11:55:35 -07001473 if fs.isfile(logname):
1474 fs.remove(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001475 # Also delete any pid specific file from previous traces.
1476 for i in glob.iglob(logname + '.*'):
1477 if i.rsplit('.', 1)[1].isdigit():
maruel12e30012015-10-09 11:55:35 -07001478 fs.remove(i)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001479
1480 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001481 def parse_log(cls, logname, blacklist, trace_name):
1482 logging.info('parse_log(%s, ..., %s)', logname, trace_name)
maruel@chromium.org8b056ba2012-10-16 14:04:49 +00001483 assert os.path.isabs(logname)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001484 data = tools.read_json(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001485 out = []
1486 for item in data['traces']:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001487 if trace_name and item['trace'] != trace_name:
1488 continue
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001489 result = {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001490 'output': item['output'],
maruel@chromium.org964ddde2012-10-29 18:13:46 +00001491 'trace': item['trace'],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001492 }
1493 try:
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001494 context = cls.Context(blacklist, item['pid'], item['cwd'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001495 for pidfile in glob.iglob('%s.%s.*' % (logname, item['trace'])):
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001496 logging.debug('Reading %s', pidfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001497 pid = pidfile.rsplit('.', 1)[1]
1498 if pid.isdigit():
1499 pid = int(pid)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001500 found_line = False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001501 for line in open(pidfile, 'rb'):
1502 context.on_line(pid, line)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001503 found_line = True
1504 if not found_line:
1505 # Ensures that a completely empty trace still creates the
1506 # corresponding Process instance by logging a dummy line.
1507 context.on_line(pid, '')
1508 else:
1509 logging.warning('Found unexpected file %s', pidfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 result['results'] = context.to_results()
maruel@chromium.org424f5692012-10-15 15:42:38 +00001511 except TracingFailure:
1512 result['exception'] = sys.exc_info()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001513 out.append(result)
1514 return out
1515
1516
1517class Dtrace(ApiBase):
1518 """Uses DTrace framework through dtrace. Requires root access.
1519
1520 Implies Mac OSX.
1521
1522 dtruss can't be used because it has compatibility issues with python.
1523
1524 Also, the pid->cwd handling needs to be done manually since OSX has no way to
1525 get the absolute path of the 'cwd' dtrace variable from the probe.
1526
1527 Also, OSX doesn't populate curpsinfo->pr_psargs properly, see
1528 https://discussions.apple.com/thread/1980539. So resort to handling execve()
1529 manually.
1530
1531 errno is not printed in the log since this implementation currently only cares
1532 about files that were successfully opened.
1533 """
1534 class Context(ApiBase.Context):
1535 # Format: index pid function(args)
1536 RE_HEADER = re.compile(r'^\d+ (\d+) ([a-zA-Z_\-]+)\((.*?)\)$')
1537
1538 # Arguments parsing.
1539 RE_DTRACE_BEGIN = re.compile(r'^\"(.+?)\"$')
1540 RE_CHDIR = re.compile(r'^\"(.+?)\"$')
1541 RE_EXECVE = re.compile(r'^\"(.+?)\", \[(\d+), (.+)\]$')
1542 RE_OPEN = re.compile(r'^\"(.+?)\", (0x[0-9a-z]+), (0x[0-9a-z]+)$')
1543 RE_PROC_START = re.compile(r'^(\d+), \"(.+?)\", (\d+)$')
1544 RE_RENAME = re.compile(r'^\"(.+?)\", \"(.+?)\"$')
1545
maruel@chromium.orgac361162013-06-04 15:54:06 +00001546 # O_DIRECTORY is not defined on Windows and dtrace doesn't exist on Windows.
1547 O_DIRECTORY = os.O_DIRECTORY if hasattr(os, 'O_DIRECTORY') else None
maruel@chromium.org538141b2013-06-03 20:57:17 +00001548 O_RDWR = os.O_RDWR
1549 O_WRONLY = os.O_WRONLY
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001550
1551 class Process(ApiBase.Context.Process):
1552 def __init__(self, *args):
1553 super(Dtrace.Context.Process, self).__init__(*args)
1554 self.cwd = self.initial_cwd
1555
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001556 def __init__(self, blacklist, thunk_pid, initial_cwd):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001557 logging.info(
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001558 '%s(%d, %s)' % (self.__class__.__name__, thunk_pid, initial_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001559 super(Dtrace.Context, self).__init__(blacklist)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001560 assert isinstance(initial_cwd, unicode), initial_cwd
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001561 # Process ID of the temporary script created by create_subprocess_thunk().
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001562 self._thunk_pid = thunk_pid
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001563 self._initial_cwd = initial_cwd
1564 self._line_number = 0
1565
1566 def on_line(self, line):
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001567 assert isinstance(line, unicode), line
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001568 self._line_number += 1
1569 match = self.RE_HEADER.match(line)
1570 if not match:
1571 raise TracingFailure(
1572 'Found malformed line: %s' % line,
1573 None,
1574 self._line_number,
1575 line)
1576 fn = getattr(
1577 self,
1578 'handle_%s' % match.group(2).replace('-', '_'),
1579 self._handle_ignored)
1580 # It is guaranteed to succeed because of the regexp. Or at least I thought
1581 # it would.
1582 pid = int(match.group(1))
1583 try:
1584 return fn(pid, match.group(3))
1585 except TracingFailure, e:
1586 # Hack in the values since the handler could be a static function.
1587 e.pid = pid
1588 e.line = line
1589 e.line_number = self._line_number
1590 # Re-raise the modified exception.
1591 raise
1592 except (KeyError, NotImplementedError, ValueError), e:
1593 raise TracingFailure(
1594 'Trace generated a %s exception: %s' % (
1595 e.__class__.__name__, str(e)),
1596 pid,
1597 self._line_number,
1598 line,
1599 e)
1600
1601 def to_results(self):
1602 process = self.root_process.to_results_process()
1603 # Internal concistency check.
1604 if sorted(self._process_lookup) != sorted(p.pid for p in process.all):
1605 raise TracingFailure(
1606 'Found internal inconsitency in process lifetime detection '
1607 'while looking for len(tree) == len(list)',
1608 None,
1609 None,
1610 None,
1611 sorted(self._process_lookup),
1612 sorted(p.pid for p in process.all))
1613 return Results(process)
1614
1615 def handle_dtrace_BEGIN(self, _pid, args):
1616 if not self.RE_DTRACE_BEGIN.match(args):
1617 raise TracingFailure(
1618 'Found internal inconsitency in dtrace_BEGIN log line',
1619 None, None, None)
1620
1621 def handle_proc_start(self, pid, args):
1622 """Transfers cwd.
1623
1624 The dtrace script already takes care of only tracing the processes that
1625 are child of the traced processes so there is no need to verify the
1626 process hierarchy.
1627 """
1628 if pid in self._process_lookup:
1629 raise TracingFailure(
1630 'Found internal inconsitency in proc_start: %d started two times' %
1631 pid,
1632 None, None, None)
1633 match = self.RE_PROC_START.match(args)
1634 if not match:
1635 raise TracingFailure(
1636 'Failed to parse arguments: %s' % args,
1637 None, None, None)
1638 ppid = int(match.group(1))
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001639 if ppid == self._thunk_pid and not self.root_process:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001640 proc = self.root_process = self.Process(
1641 self.blacklist, pid, self._initial_cwd)
1642 elif ppid in self._process_lookup:
1643 proc = self.Process(self.blacklist, pid, self._process_lookup[ppid].cwd)
1644 self._process_lookup[ppid].children.append(proc)
1645 else:
1646 # Another process tree, ignore.
1647 return
1648 self._process_lookup[pid] = proc
1649 logging.debug(
1650 'New child: %s -> %d cwd:%s' %
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001651 (ppid, pid, render(proc.initial_cwd)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001652
1653 def handle_proc_exit(self, pid, _args):
1654 """Removes cwd."""
1655 if pid in self._process_lookup:
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00001656 # self._thunk_pid is not traced itself and other traces run neither.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001657 self._process_lookup[pid].cwd = None
1658
1659 def handle_execve(self, pid, args):
1660 """Sets the process' executable.
1661
1662 TODO(maruel): Read command line arguments. See
1663 https://discussions.apple.com/thread/1980539 for an example.
1664 https://gist.github.com/1242279
1665
1666 Will have to put the answer at http://stackoverflow.com/questions/7556249.
1667 :)
1668 """
1669 if not pid in self._process_lookup:
1670 # Another process tree, ignore.
1671 return
1672 match = self.RE_EXECVE.match(args)
1673 if not match:
1674 raise TracingFailure(
1675 'Failed to parse arguments: %r' % args,
1676 None, None, None)
1677 proc = self._process_lookup[pid]
1678 proc.executable = match.group(1)
maruel@chromium.org538141b2013-06-03 20:57:17 +00001679 self._handle_file(pid, proc.executable, Results.File.READ)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001680 proc.command = self.process_escaped_arguments(match.group(3))
1681 if int(match.group(2)) != len(proc.command):
1682 raise TracingFailure(
1683 'Failed to parse execve() arguments: %s' % args,
1684 None, None, None)
1685
1686 def handle_chdir(self, pid, args):
1687 """Updates cwd."""
1688 if pid not in self._process_lookup:
1689 # Another process tree, ignore.
1690 return
1691 cwd = self.RE_CHDIR.match(args).group(1)
1692 if not cwd.startswith('/'):
1693 cwd2 = os.path.join(self._process_lookup[pid].cwd, cwd)
1694 logging.debug('handle_chdir(%d, %s) -> %s' % (pid, cwd, cwd2))
1695 else:
1696 logging.debug('handle_chdir(%d, %s)' % (pid, cwd))
1697 cwd2 = cwd
1698 self._process_lookup[pid].cwd = cwd2
1699
1700 def handle_open_nocancel(self, pid, args):
1701 """Redirects to handle_open()."""
1702 return self.handle_open(pid, args)
1703
1704 def handle_open(self, pid, args):
1705 if pid not in self._process_lookup:
1706 # Another process tree, ignore.
1707 return
1708 match = self.RE_OPEN.match(args)
1709 if not match:
1710 raise TracingFailure(
1711 'Failed to parse arguments: %s' % args,
1712 None, None, None)
1713 flag = int(match.group(2), 16)
1714 if self.O_DIRECTORY & flag == self.O_DIRECTORY:
1715 # Ignore directories.
1716 return
maruel@chromium.org538141b2013-06-03 20:57:17 +00001717 self._handle_file(
1718 pid,
1719 match.group(1),
1720 Results.File.READ if not ((self.O_RDWR | self.O_WRONLY) & flag)
1721 else Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001722
1723 def handle_rename(self, pid, args):
1724 if pid not in self._process_lookup:
1725 # Another process tree, ignore.
1726 return
1727 match = self.RE_RENAME.match(args)
1728 if not match:
1729 raise TracingFailure(
1730 'Failed to parse arguments: %s' % args,
1731 None, None, None)
maruel@chromium.org538141b2013-06-03 20:57:17 +00001732 self._handle_file(pid, match.group(1), Results.File.READ)
1733 self._handle_file(pid, match.group(2), Results.File.WRITE)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001734
maruel@chromium.org538141b2013-06-03 20:57:17 +00001735 def _handle_file(self, pid, filepath, mode):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001736 if not filepath.startswith('/'):
1737 filepath = os.path.join(self._process_lookup[pid].cwd, filepath)
1738 # We can get '..' in the path.
1739 filepath = os.path.normpath(filepath)
1740 # Sadly, still need to filter out directories here;
1741 # saw open_nocancel(".", 0, 0) = 0 lines.
maruel12e30012015-10-09 11:55:35 -07001742 if fs.isdir(filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001743 return
maruel@chromium.org538141b2013-06-03 20:57:17 +00001744 self._process_lookup[pid].add_file(filepath, mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001745
1746 def handle_ftruncate(self, pid, args):
1747 """Just used as a signal to kill dtrace, ignoring."""
1748 pass
1749
1750 @staticmethod
1751 def _handle_ignored(pid, args):
1752 """Is called for all the event traces that are not handled."""
1753 raise NotImplementedError('Please implement me')
1754
1755 @staticmethod
1756 def process_escaped_arguments(text):
1757 """Extracts escaped arguments on a string and return the arguments as a
1758 list.
1759
1760 Implemented as an automaton.
1761
1762 Example:
1763 With text = '\\001python2.7\\001-c\\001print(\\"hi\\")\\0', the
1764 function will return ['python2.7', '-c', 'print("hi")]
1765 """
1766 if not text.endswith('\\0'):
1767 raise ValueError('String is not null terminated: %r' % text, text)
1768 text = text[:-2]
1769
1770 def unescape(x):
1771 """Replaces '\\' with '\' and '\?' (where ? is anything) with ?."""
1772 out = []
1773 escaped = False
1774 for i in x:
1775 if i == '\\' and not escaped:
1776 escaped = True
1777 continue
1778 escaped = False
1779 out.append(i)
1780 return ''.join(out)
1781
1782 return [unescape(i) for i in text.split('\\001')]
1783
1784 class Tracer(ApiBase.Tracer):
1785 # pylint: disable=C0301
1786 #
1787 # To understand the following code, you'll want to take a look at:
1788 # http://developers.sun.com/solaris/articles/dtrace_quickref/dtrace_quickref.html
1789 # https://wikis.oracle.com/display/DTrace/Variables
1790 # http://docs.oracle.com/cd/E19205-01/820-4221/
1791 #
1792 # 0. Dump all the valid probes into a text file. It is important, you
1793 # want to redirect into a file and you don't want to constantly 'sudo'.
1794 # $ sudo dtrace -l > probes.txt
1795 #
1796 # 1. Count the number of probes:
1797 # $ wc -l probes.txt
1798 # 81823 # On OSX 10.7, including 1 header line.
1799 #
1800 # 2. List providers, intentionally skipping all the 'syspolicy10925' and the
1801 # likes and skipping the header with NR>1:
1802 # $ awk 'NR>1 { print $2 }' probes.txt | sort | uniq | grep -v '[[:digit:]]'
1803 # dtrace
1804 # fbt
1805 # io
1806 # ip
1807 # lockstat
1808 # mach_trap
1809 # proc
1810 # profile
1811 # sched
1812 # syscall
1813 # tcp
1814 # vminfo
1815 #
1816 # 3. List of valid probes:
1817 # $ grep syscall probes.txt | less
1818 # or use dtrace directly:
1819 # $ sudo dtrace -l -P syscall | less
1820 #
1821 # trackedpid is an associative array where its value can be 0, 1 or 2.
1822 # 0 is for untracked processes and is the default value for items not
1823 # in the associative array.
1824 # 1 is for tracked processes.
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00001825 # 2 is for the script created by create_subprocess_thunk() only. It is not
1826 # tracked itself but all its decendants are.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001827 #
1828 # The script will kill itself only once waiting_to_die == 1 and
1829 # current_processes == 0, so that both getlogin() was called and that
1830 # all traced processes exited.
1831 #
1832 # TODO(maruel): Use cacheable predicates. See
1833 # https://wikis.oracle.com/display/DTrace/Performance+Considerations
1834 D_CODE = """
1835 dtrace:::BEGIN {
1836 waiting_to_die = 0;
1837 current_processes = 0;
1838 logindex = 0;
1839 printf("%d %d %s_%s(\\"%s\\")\\n",
1840 logindex, PID, probeprov, probename, SCRIPT);
1841 logindex++;
1842 }
1843
1844 proc:::start /trackedpid[ppid]/ {
1845 trackedpid[pid] = 1;
1846 current_processes += 1;
1847 printf("%d %d %s_%s(%d, \\"%s\\", %d)\\n",
1848 logindex, pid, probeprov, probename,
1849 ppid,
1850 execname,
1851 current_processes);
1852 logindex++;
1853 }
1854 /* Should use SCRIPT but there is no access to this variable at that
1855 * point. */
1856 proc:::start /ppid == PID && execname == "Python"/ {
1857 trackedpid[pid] = 2;
1858 current_processes += 1;
1859 printf("%d %d %s_%s(%d, \\"%s\\", %d)\\n",
1860 logindex, pid, probeprov, probename,
1861 ppid,
1862 execname,
1863 current_processes);
1864 logindex++;
1865 }
1866 proc:::exit /trackedpid[pid] &&
1867 current_processes == 1 &&
1868 waiting_to_die == 1/ {
1869 trackedpid[pid] = 0;
1870 current_processes -= 1;
1871 printf("%d %d %s_%s(%d)\\n",
1872 logindex, pid, probeprov, probename,
1873 current_processes);
1874 logindex++;
1875 exit(0);
1876 }
1877 proc:::exit /trackedpid[pid]/ {
1878 trackedpid[pid] = 0;
1879 current_processes -= 1;
1880 printf("%d %d %s_%s(%d)\\n",
1881 logindex, pid, probeprov, probename,
1882 current_processes);
1883 logindex++;
1884 }
1885
1886 /* Use an arcane function to detect when we need to die */
1887 syscall::ftruncate:entry /pid == PID && arg0 == FILE_ID/ {
1888 waiting_to_die = 1;
1889 printf("%d %d %s()\\n", logindex, pid, probefunc);
1890 logindex++;
1891 }
1892 syscall::ftruncate:entry /
1893 pid == PID && arg0 == FILE_ID && current_processes == 0/ {
1894 exit(0);
1895 }
1896
1897 syscall::open*:entry /trackedpid[pid] == 1/ {
1898 self->open_arg0 = arg0;
1899 self->open_arg1 = arg1;
1900 self->open_arg2 = arg2;
1901 }
1902 syscall::open*:return /trackedpid[pid] == 1 && errno == 0/ {
1903 this->open_arg0 = copyinstr(self->open_arg0);
1904 printf("%d %d %s(\\"%s\\", 0x%x, 0x%x)\\n",
1905 logindex, pid, probefunc,
1906 this->open_arg0,
1907 self->open_arg1,
1908 self->open_arg2);
1909 logindex++;
1910 this->open_arg0 = 0;
1911 }
1912 syscall::open*:return /trackedpid[pid] == 1/ {
1913 self->open_arg0 = 0;
1914 self->open_arg1 = 0;
1915 self->open_arg2 = 0;
1916 }
1917
1918 syscall::rename:entry /trackedpid[pid] == 1/ {
1919 self->rename_arg0 = arg0;
1920 self->rename_arg1 = arg1;
1921 }
1922 syscall::rename:return /trackedpid[pid] == 1 && errno == 0/ {
1923 this->rename_arg0 = copyinstr(self->rename_arg0);
1924 this->rename_arg1 = copyinstr(self->rename_arg1);
1925 printf("%d %d %s(\\"%s\\", \\"%s\\")\\n",
1926 logindex, pid, probefunc,
1927 this->rename_arg0,
1928 this->rename_arg1);
1929 logindex++;
1930 this->rename_arg0 = 0;
1931 this->rename_arg1 = 0;
1932 }
1933 syscall::rename:return /trackedpid[pid] == 1/ {
1934 self->rename_arg0 = 0;
1935 self->rename_arg1 = 0;
1936 }
1937
1938 /* Track chdir, it's painful because it is only receiving relative path.
1939 */
1940 syscall::chdir:entry /trackedpid[pid] == 1/ {
1941 self->chdir_arg0 = arg0;
1942 }
1943 syscall::chdir:return /trackedpid[pid] == 1 && errno == 0/ {
1944 this->chdir_arg0 = copyinstr(self->chdir_arg0);
1945 printf("%d %d %s(\\"%s\\")\\n",
1946 logindex, pid, probefunc,
1947 this->chdir_arg0);
1948 logindex++;
1949 this->chdir_arg0 = 0;
1950 }
1951 syscall::chdir:return /trackedpid[pid] == 1/ {
1952 self->chdir_arg0 = 0;
1953 }
1954 """
1955
1956 # execve-specific code, tends to throw a lot of exceptions.
1957 D_CODE_EXECVE = """
1958 /* Finally what we care about! */
1959 syscall::exec*:entry /trackedpid[pid]/ {
1960 self->exec_arg0 = copyinstr(arg0);
1961 /* Incrementally probe for a NULL in the argv parameter of execve() to
1962 * figure out argc. */
1963 /* TODO(maruel): Skip the remaining copyin() when a NULL pointer was
1964 * found. */
1965 self->exec_argc = 0;
1966 /* Probe for argc==1 */
1967 this->exec_argv = (user_addr_t*)copyin(
1968 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1969 self->exec_argc = this->exec_argv[self->exec_argc] ?
1970 (self->exec_argc + 1) : self->exec_argc;
1971
1972 /* Probe for argc==2 */
1973 this->exec_argv = (user_addr_t*)copyin(
1974 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1975 self->exec_argc = this->exec_argv[self->exec_argc] ?
1976 (self->exec_argc + 1) : self->exec_argc;
1977
1978 /* Probe for argc==3 */
1979 this->exec_argv = (user_addr_t*)copyin(
1980 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1981 self->exec_argc = this->exec_argv[self->exec_argc] ?
1982 (self->exec_argc + 1) : self->exec_argc;
1983
1984 /* Probe for argc==4 */
1985 this->exec_argv = (user_addr_t*)copyin(
1986 arg1, sizeof(user_addr_t) * (self->exec_argc + 1));
1987 self->exec_argc = this->exec_argv[self->exec_argc] ?
1988 (self->exec_argc + 1) : self->exec_argc;
1989
1990 /* Copy the inputs strings since there is no guarantee they'll be
1991 * present after the call completed. */
1992 self->exec_argv0 = (self->exec_argc > 0) ?
1993 copyinstr(this->exec_argv[0]) : "";
1994 self->exec_argv1 = (self->exec_argc > 1) ?
1995 copyinstr(this->exec_argv[1]) : "";
1996 self->exec_argv2 = (self->exec_argc > 2) ?
1997 copyinstr(this->exec_argv[2]) : "";
1998 self->exec_argv3 = (self->exec_argc > 3) ?
1999 copyinstr(this->exec_argv[3]) : "";
2000 this->exec_argv = 0;
2001 }
2002 syscall::exec*:return /trackedpid[pid] && errno == 0/ {
2003 /* We need to join strings here, as using multiple printf() would
2004 * cause tearing when multiple threads/processes are traced.
2005 * Since it is impossible to escape a string and join it to another one,
2006 * like sprintf("%s%S", previous, more), use hackery.
2007 * Each of the elements are split with a \\1. \\0 cannot be used because
2008 * it is simply ignored. This will conflict with any program putting a
2009 * \\1 in their execve() string but this should be "rare enough" */
2010 this->args = "";
2011 /* Process exec_argv[0] */
2012 this->args = strjoin(
2013 this->args, (self->exec_argc > 0) ? self->exec_argv0 : "");
2014
2015 /* Process exec_argv[1] */
2016 this->args = strjoin(
2017 this->args, (self->exec_argc > 1) ? "\\1" : "");
2018 this->args = strjoin(
2019 this->args, (self->exec_argc > 1) ? self->exec_argv1 : "");
2020
2021 /* Process exec_argv[2] */
2022 this->args = strjoin(
2023 this->args, (self->exec_argc > 2) ? "\\1" : "");
2024 this->args = strjoin(
2025 this->args, (self->exec_argc > 2) ? self->exec_argv2 : "");
2026
2027 /* Process exec_argv[3] */
2028 this->args = strjoin(
2029 this->args, (self->exec_argc > 3) ? "\\1" : "");
2030 this->args = strjoin(
2031 this->args, (self->exec_argc > 3) ? self->exec_argv3 : "");
2032
2033 /* Prints self->exec_argc to permits verifying the internal
2034 * consistency since this code is quite fishy. */
2035 printf("%d %d %s(\\"%s\\", [%d, %S])\\n",
2036 logindex, pid, probefunc,
2037 self->exec_arg0,
2038 self->exec_argc,
2039 this->args);
2040 logindex++;
2041 this->args = 0;
2042 }
2043 syscall::exec*:return /trackedpid[pid]/ {
2044 self->exec_arg0 = 0;
2045 self->exec_argc = 0;
2046 self->exec_argv0 = 0;
2047 self->exec_argv1 = 0;
2048 self->exec_argv2 = 0;
2049 self->exec_argv3 = 0;
2050 }
2051 """
2052
2053 # Code currently not used.
2054 D_EXTRANEOUS = """
2055 /* This is a good learning experience, since it traces a lot of things
2056 * related to the process and child processes.
2057 * Warning: it generates a gigantic log. For example, tracing
2058 * "data/trace_inputs/child1.py --child" generates a 2mb log and takes
2059 * several minutes to execute.
2060 */
2061 /*
2062 mach_trap::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2063 printf("%d %d %s_%s() = %d\\n",
2064 logindex, pid, probeprov, probefunc, errno);
2065 logindex++;
2066 }
2067 proc::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2068 printf("%d %d %s_%s() = %d\\n",
2069 logindex, pid, probeprov, probefunc, errno);
2070 logindex++;
2071 }
2072 sched::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2073 printf("%d %d %s_%s() = %d\\n",
2074 logindex, pid, probeprov, probefunc, errno);
2075 logindex++;
2076 }
2077 syscall::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2078 printf("%d %d %s_%s() = %d\\n",
2079 logindex, pid, probeprov, probefunc, errno);
2080 logindex++;
2081 }
2082 vminfo::: /trackedpid[pid] == 1 || trackedpid[ppid]/ {
2083 printf("%d %d %s_%s() = %d\\n",
2084 logindex, pid, probeprov, probefunc, errno);
2085 logindex++;
2086 }
2087 */
2088 /* TODO(maruel): *stat* functions and friends
2089 syscall::access:return,
2090 syscall::chdir:return,
2091 syscall::chflags:return,
2092 syscall::chown:return,
2093 syscall::chroot:return,
2094 syscall::getattrlist:return,
2095 syscall::getxattr:return,
2096 syscall::lchown:return,
2097 syscall::lstat64:return,
2098 syscall::lstat:return,
2099 syscall::mkdir:return,
2100 syscall::pathconf:return,
2101 syscall::readlink:return,
2102 syscall::removexattr:return,
2103 syscall::setxattr:return,
2104 syscall::stat64:return,
2105 syscall::stat:return,
2106 syscall::truncate:return,
2107 syscall::unlink:return,
2108 syscall::utimes:return,
2109 */
2110 """
2111
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00002112 def __init__(self, logname, use_sudo):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002113 """Starts the log collection with dtrace.
2114
2115 Requires root access or chmod 4555 on dtrace. dtrace is asynchronous so
2116 this needs to wait for dtrace to be "warmed up".
2117 """
2118 super(Dtrace.Tracer, self).__init__(logname)
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002119 # This script is used as a signal to figure out the root process.
2120 self._signal_script = create_subprocess_thunk()
2121 self._scripts_to_cleanup.append(self._signal_script)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002122 # This unique dummy temp file is used to signal the dtrace script that it
2123 # should stop as soon as all the child processes are done. A bit hackish
2124 # but works fine enough.
2125 self._dummy_file_id, self._dummy_file_name = tempfile.mkstemp(
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -04002126 prefix=u'trace_signal_file')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002127
maruel@chromium.org9b2112a2013-04-05 18:01:02 +00002128 dtrace_path = '/usr/sbin/dtrace'
maruel12e30012015-10-09 11:55:35 -07002129 if not fs.isfile(dtrace_path):
maruel@chromium.org9b2112a2013-04-05 18:01:02 +00002130 dtrace_path = 'dtrace'
maruel12e30012015-10-09 11:55:35 -07002131 elif use_sudo is None and (fs.stat(dtrace_path).st_mode & stat.S_ISUID):
maruel@chromium.org9b2112a2013-04-05 18:01:02 +00002132 # No need to sudo. For those following at home, don't do that.
2133 use_sudo = False
2134
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002135 # Note: do not use the -p flag. It's useless if the initial process quits
2136 # too fast, resulting in missing traces from the grand-children. The D
2137 # code manages the dtrace lifetime itself.
2138 trace_cmd = [
maruel@chromium.org9b2112a2013-04-05 18:01:02 +00002139 dtrace_path,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002140 # Use a larger buffer if getting 'out of scratch space' errors.
2141 # Ref: https://wikis.oracle.com/display/DTrace/Options+and+Tunables
2142 '-b', '10m',
2143 '-x', 'dynvarsize=10m',
2144 #'-x', 'dtrace_global_maxsize=1m',
2145 '-x', 'evaltime=exec',
2146 '-o', '/dev/stderr',
2147 '-q',
2148 '-n', self._get_dtrace_code(),
2149 ]
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00002150 if use_sudo is not False:
2151 trace_cmd.insert(0, 'sudo')
2152
maruel12e30012015-10-09 11:55:35 -07002153 with fs.open(self._logname + '.log', 'wb') as logfile:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002154 self._dtrace = subprocess.Popen(
2155 trace_cmd, stdout=logfile, stderr=subprocess.STDOUT)
2156 logging.debug('Started dtrace pid: %d' % self._dtrace.pid)
2157
2158 # Reads until one line is printed, which signifies dtrace is up and ready.
maruel12e30012015-10-09 11:55:35 -07002159 with fs.open(self._logname + '.log', 'rb') as logfile:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002160 while 'dtrace_BEGIN' not in logfile.readline():
2161 if self._dtrace.poll() is not None:
2162 # Do a busy wait. :/
2163 break
2164 logging.debug('dtrace started')
2165
2166 def _get_dtrace_code(self):
2167 """Setups the D code to implement child process tracking.
2168
2169 Injects the cookie in the script so it knows when to stop.
2170
2171 The script will detect any instance of the script created with
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002172 create_subprocess_thunk() and will start tracing it.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002173 """
maruel@chromium.orgbe75f282012-11-02 19:34:37 +00002174 out = (
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002175 'inline int PID = %d;\n'
2176 'inline string SCRIPT = "%s";\n'
2177 'inline int FILE_ID = %d;\n'
2178 '\n'
2179 '%s') % (
2180 os.getpid(),
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002181 self._signal_script,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002182 self._dummy_file_id,
maruel@chromium.orgbe75f282012-11-02 19:34:37 +00002183 self.D_CODE)
2184 if os.environ.get('TRACE_INPUTS_DTRACE_ENABLE_EXECVE') == '1':
2185 # Do not enable by default since it tends to spew dtrace: error lines
2186 # because the execve() parameters are not in valid memory at the time of
2187 # logging.
2188 # TODO(maruel): Find a way to make this reliable since it's useful but
2189 # only works in limited/trivial uses cases for now.
2190 out += self.D_CODE_EXECVE
2191 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002192
2193 def trace(self, cmd, cwd, tracename, output):
2194 """Runs dtrace on an executable.
2195
2196 This dtruss is broken when it starts the process itself or when tracing
2197 child processes, this code starts a wrapper process
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002198 generated with create_subprocess_thunk() which starts the executable to
2199 trace.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002200 """
2201 logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
2202 assert os.path.isabs(cmd[0]), cmd[0]
2203 assert os.path.isabs(cwd), cwd
2204 assert os.path.normpath(cwd) == cwd, cwd
2205 with self._lock:
2206 if not self._initialized:
2207 raise TracingFailure(
2208 'Called Tracer.trace() on an unitialized object',
2209 None, None, None, tracename)
2210 assert tracename not in (i['trace'] for i in self._traces)
2211
2212 # Starts the script wrapper to start the child process. This signals the
2213 # dtrace script that this process is to be traced.
2214 stdout = stderr = None
2215 if output:
2216 stdout = subprocess.PIPE
2217 stderr = subprocess.STDOUT
2218 child_cmd = [
2219 sys.executable,
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002220 self._signal_script,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002221 tracename,
2222 ]
2223 # Call a dummy function so that dtrace knows I'm about to launch a process
2224 # that needs to be traced.
2225 # Yummy.
2226 child = subprocess.Popen(
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002227 child_cmd + tools.fix_python_path(cmd),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002228 stdin=subprocess.PIPE,
2229 stdout=stdout,
2230 stderr=stderr,
2231 cwd=cwd)
2232 logging.debug('Started child pid: %d' % child.pid)
2233
2234 out = child.communicate()[0]
2235 # This doesn't mean tracing is done, one of the grand-child process may
2236 # still be alive. It will be tracked with the dtrace script.
2237
2238 with self._lock:
2239 assert tracename not in (i['trace'] for i in self._traces)
2240 self._traces.append(
2241 {
2242 'cmd': cmd,
2243 'cwd': cwd,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002244 'pid': child.pid,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002245 'output': out,
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002246 'trace': tracename,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002247 })
2248 return child.returncode, out
2249
2250 def close(self, timeout=None):
2251 """Terminates dtrace."""
2252 logging.debug('close(%s)' % timeout)
2253 try:
2254 try:
2255 super(Dtrace.Tracer, self).close(timeout)
2256 # Signal dtrace that it should stop now.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00002257 # ftruncate doesn't exist on Windows.
2258 os.ftruncate(self._dummy_file_id, 0) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002259 if timeout:
2260 start = time.time()
2261 # Use polling. :/
2262 while (self._dtrace.poll() is None and
2263 (time.time() - start) < timeout):
2264 time.sleep(0.1)
2265 self._dtrace.kill()
2266 self._dtrace.wait()
2267 finally:
2268 # Make sure to kill it in any case.
2269 if self._dtrace.poll() is None:
2270 try:
2271 self._dtrace.kill()
2272 self._dtrace.wait()
2273 except OSError:
2274 pass
2275
2276 if self._dtrace.returncode != 0:
2277 # Warn about any dtrace failure but basically ignore it.
2278 print 'dtrace failure: %s' % self._dtrace.returncode
2279 finally:
2280 os.close(self._dummy_file_id)
maruel12e30012015-10-09 11:55:35 -07002281 fs.remove(self._dummy_file_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002282
2283 def post_process_log(self):
2284 """Sorts the log back in order when each call occured.
2285
2286 dtrace doesn't save the buffer in strict order since it keeps one buffer
2287 per CPU.
2288 """
2289 super(Dtrace.Tracer, self).post_process_log()
2290 logname = self._logname + '.log'
maruel12e30012015-10-09 11:55:35 -07002291 with fs.open(logname, 'rb') as logfile:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002292 lines = [l for l in logfile if l.strip()]
2293 errors = [l for l in lines if l.startswith('dtrace:')]
2294 if errors:
2295 raise TracingFailure(
2296 'Found errors in the trace: %s' % '\n'.join(errors),
2297 None, None, None, logname)
2298 try:
2299 lines = sorted(lines, key=lambda l: int(l.split(' ', 1)[0]))
2300 except ValueError:
2301 raise TracingFailure(
2302 'Found errors in the trace: %s' % '\n'.join(
2303 l for l in lines if l.split(' ', 1)[0].isdigit()),
2304 None, None, None, logname)
maruel12e30012015-10-09 11:55:35 -07002305 with fs.open(logname, 'wb') as logfile:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002306 logfile.write(''.join(lines))
2307
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00002308 def __init__(self, use_sudo=None):
2309 super(Dtrace, self).__init__()
2310 self.use_sudo = use_sudo
2311
2312 def get_tracer(self, logname):
2313 return self.Tracer(logname, self.use_sudo)
2314
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002315 @staticmethod
2316 def clean_trace(logname):
2317 for ext in ('', '.log'):
maruel12e30012015-10-09 11:55:35 -07002318 if fs.isfile(logname + ext):
2319 fs.remove(logname + ext)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002320
2321 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00002322 def parse_log(cls, logname, blacklist, trace_name):
2323 logging.info('parse_log(%s, ..., %s)', logname, trace_name)
maruel@chromium.org8b056ba2012-10-16 14:04:49 +00002324 assert os.path.isabs(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002325
2326 def blacklist_more(filepath):
2327 # All the HFS metadata is in the form /.vol/...
2328 return blacklist(filepath) or re.match(r'^\/\.vol\/.+$', filepath)
2329
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05002330 data = tools.read_json(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002331 out = []
2332 for item in data['traces']:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00002333 if trace_name and item['trace'] != trace_name:
2334 continue
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002335 result = {
2336 'output': item['output'],
2337 'trace': item['trace'],
2338 }
2339 try:
2340 context = cls.Context(blacklist_more, item['pid'], item['cwd'])
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002341 # It's fine to assume the file as UTF-8: OSX enforces the file names to
2342 # be valid UTF-8 and we control the log output.
2343 for line in codecs.open(logname + '.log', 'rb', encoding='utf-8'):
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002344 context.on_line(line)
2345 result['results'] = context.to_results()
2346 except TracingFailure:
2347 result['exception'] = sys.exc_info()
2348 out.append(result)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002349 return out
2350
2351
2352class LogmanTrace(ApiBase):
2353 """Uses the native Windows ETW based tracing functionality to trace a child
2354 process.
2355
2356 Caveat: this implementations doesn't track cwd or initial_cwd. It is because
2357 the Windows Kernel doesn't have a concept of 'current working directory' at
2358 all. A Win32 process has a map of current directories, one per drive letter
2359 and it is managed by the user mode kernel32.dll. In kernel, a file is always
2360 opened relative to another file_object or as an absolute path. All the current
2361 working directory logic is done in user mode.
2362 """
2363 class Context(ApiBase.Context):
2364 """Processes a ETW log line and keeps the list of existent and non
2365 existent files accessed.
2366
2367 Ignores directories.
2368 """
2369 # These indexes are for the stripped version in json.
2370 EVENT_NAME = 0
2371 TYPE = 1
2372 PID = 2
2373 TID = 3
2374 PROCESSOR_ID = 4
2375 TIMESTAMP = 5
2376 USER_DATA = 6
2377
2378 class Process(ApiBase.Context.Process):
2379 def __init__(self, *args):
2380 super(LogmanTrace.Context.Process, self).__init__(*args)
2381 # Handle file objects that succeeded.
2382 self.file_objects = {}
2383
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002384 def __init__(self, blacklist, thunk_pid, trace_name, thunk_cmd):
2385 logging.info(
2386 '%s(%d, %s, %s)', self.__class__.__name__, thunk_pid, trace_name,
2387 thunk_cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002388 super(LogmanTrace.Context, self).__init__(blacklist)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002389 self._drive_map = file_path.DosDriveMap()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002390 # Threads mapping to the corresponding process id.
2391 self._threads_active = {}
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002392 # Process ID of the tracer, e.g. the temporary script created by
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002393 # create_subprocess_thunk(). This is tricky because the process id may
2394 # have been reused.
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002395 self._thunk_pid = thunk_pid
2396 self._thunk_cmd = thunk_cmd
2397 self._trace_name = trace_name
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002398 self._line_number = 0
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002399 self._thunk_process = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002400
2401 def on_line(self, line):
2402 """Processes a json Event line."""
2403 self._line_number += 1
2404 try:
2405 # By Opcode
2406 handler = getattr(
2407 self,
2408 'handle_%s_%s' % (line[self.EVENT_NAME], line[self.TYPE]),
2409 None)
2410 if not handler:
2411 raise TracingFailure(
2412 'Unexpected event %s_%s' % (
2413 line[self.EVENT_NAME], line[self.TYPE]),
2414 None, None, None)
2415 handler(line)
2416 except TracingFailure, e:
2417 # Hack in the values since the handler could be a static function.
2418 e.pid = line[self.PID]
2419 e.line = line
2420 e.line_number = self._line_number
2421 # Re-raise the modified exception.
2422 raise
2423 except (KeyError, NotImplementedError, ValueError), e:
2424 raise TracingFailure(
2425 'Trace generated a %s exception: %s' % (
2426 e.__class__.__name__, str(e)),
2427 line[self.PID],
2428 self._line_number,
2429 line,
2430 e)
2431
2432 def to_results(self):
2433 if not self.root_process:
2434 raise TracingFailure(
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002435 'Failed to detect the initial process %d' % self._thunk_pid,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002436 None, None, None)
2437 process = self.root_process.to_results_process()
2438 return Results(process)
2439
2440 def _thread_to_process(self, tid):
2441 """Finds the process from the thread id."""
2442 tid = int(tid, 16)
2443 pid = self._threads_active.get(tid)
2444 if not pid or not self._process_lookup.get(pid):
2445 return
2446 return self._process_lookup[pid]
2447
2448 @classmethod
2449 def handle_EventTrace_Header(cls, line):
2450 """Verifies no event was dropped, e.g. no buffer overrun occured."""
2451 BUFFER_SIZE = cls.USER_DATA
2452 #VERSION = cls.USER_DATA + 1
2453 #PROVIDER_VERSION = cls.USER_DATA + 2
2454 #NUMBER_OF_PROCESSORS = cls.USER_DATA + 3
2455 #END_TIME = cls.USER_DATA + 4
2456 #TIMER_RESOLUTION = cls.USER_DATA + 5
2457 #MAX_FILE_SIZE = cls.USER_DATA + 6
2458 #LOG_FILE_MODE = cls.USER_DATA + 7
2459 #BUFFERS_WRITTEN = cls.USER_DATA + 8
2460 #START_BUFFERS = cls.USER_DATA + 9
2461 #POINTER_SIZE = cls.USER_DATA + 10
2462 EVENTS_LOST = cls.USER_DATA + 11
2463 #CPU_SPEED = cls.USER_DATA + 12
2464 #LOGGER_NAME = cls.USER_DATA + 13
2465 #LOG_FILE_NAME = cls.USER_DATA + 14
2466 #BOOT_TIME = cls.USER_DATA + 15
2467 #PERF_FREQ = cls.USER_DATA + 16
2468 #START_TIME = cls.USER_DATA + 17
2469 #RESERVED_FLAGS = cls.USER_DATA + 18
2470 #BUFFERS_LOST = cls.USER_DATA + 19
2471 #SESSION_NAME_STRING = cls.USER_DATA + 20
2472 #LOG_FILE_NAME_STRING = cls.USER_DATA + 21
2473 if line[EVENTS_LOST] != '0':
2474 raise TracingFailure(
2475 ( '%s events were lost during trace, please increase the buffer '
2476 'size from %s') % (line[EVENTS_LOST], line[BUFFER_SIZE]),
2477 None, None, None)
2478
2479 def handle_FileIo_Cleanup(self, line):
2480 """General wisdom: if a file is closed, it's because it was opened.
2481
2482 Note that FileIo_Close is not used since if a file was opened properly but
2483 not closed before the process exits, only Cleanup will be logged.
2484 """
2485 #IRP = self.USER_DATA
2486 TTID = self.USER_DATA + 1 # Thread ID, that's what we want.
2487 FILE_OBJECT = self.USER_DATA + 2
2488 #FILE_KEY = self.USER_DATA + 3
2489 proc = self._thread_to_process(line[TTID])
2490 if not proc:
2491 # Not a process we care about.
2492 return
2493 file_object = line[FILE_OBJECT]
2494 if file_object in proc.file_objects:
maruel@chromium.org538141b2013-06-03 20:57:17 +00002495 filepath, access_type = proc.file_objects.pop(file_object)
2496 proc.add_file(filepath, access_type)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002497
2498 def handle_FileIo_Create(self, line):
2499 """Handles a file open.
2500
2501 All FileIo events are described at
2502 http://msdn.microsoft.com/library/windows/desktop/aa363884.aspx
2503 for some value of 'description'.
2504
2505 " (..) process and thread id values of the IO events (..) are not valid "
2506 http://msdn.microsoft.com/magazine/ee358703.aspx
2507
2508 The FileIo.Create event doesn't return if the CreateFile() call
2509 succeeded, so keep track of the file_object and check that it is
2510 eventually closed with FileIo_Cleanup.
2511 """
2512 #IRP = self.USER_DATA
2513 TTID = self.USER_DATA + 1 # Thread ID, that's what we want.
2514 FILE_OBJECT = self.USER_DATA + 2
2515 #CREATE_OPTIONS = self.USER_DATA + 3
2516 #FILE_ATTRIBUTES = self.USER_DATA + 4
maruel@chromium.org538141b2013-06-03 20:57:17 +00002517 #SHARE_ACCESS = self.USER_DATA + 5
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002518 OPEN_PATH = self.USER_DATA + 6
2519
2520 proc = self._thread_to_process(line[TTID])
2521 if not proc:
2522 # Not a process we care about.
2523 return
2524
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002525 raw_path = line[OPEN_PATH]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002526 # Ignore directories and bare drive right away.
2527 if raw_path.endswith(os.path.sep):
2528 return
2529 filepath = self._drive_map.to_win32(raw_path)
2530 # Ignore bare drive right away. Some may still fall through with format
2531 # like '\\?\X:'
2532 if len(filepath) == 2:
2533 return
2534 file_object = line[FILE_OBJECT]
maruel12e30012015-10-09 11:55:35 -07002535 if fs.isdir(filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002536 # There is no O_DIRECTORY equivalent on Windows. The closed is
2537 # FILE_FLAG_BACKUP_SEMANTICS but it's not exactly right either. So
2538 # simply discard directories are they are found.
2539 return
maruel@chromium.org538141b2013-06-03 20:57:17 +00002540 # Override any stale file object.
2541 # TODO(maruel): Figure out a way to detect if the file was opened for
2542 # reading or writting. Sadly CREATE_OPTIONS doesn't seem to be of any help
2543 # here. For now mark as None to make it clear we have no idea what it is
2544 # about.
2545 proc.file_objects[file_object] = (filepath, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002546
2547 def handle_FileIo_Rename(self, line):
2548 # TODO(maruel): Handle?
2549 pass
2550
2551 def handle_Process_End(self, line):
2552 pid = line[self.PID]
2553 if self._process_lookup.get(pid):
2554 logging.info('Terminated: %d' % pid)
2555 self._process_lookup[pid] = None
2556 else:
2557 logging.debug('Terminated: %d' % pid)
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002558 if self._thunk_process and self._thunk_process.pid == pid:
2559 self._thunk_process = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002560
2561 def handle_Process_Start(self, line):
2562 """Handles a new child process started by PID."""
2563 #UNIQUE_PROCESS_KEY = self.USER_DATA
2564 PROCESS_ID = self.USER_DATA + 1
2565 #PARENT_PID = self.USER_DATA + 2
2566 #SESSION_ID = self.USER_DATA + 3
2567 #EXIT_STATUS = self.USER_DATA + 4
2568 #DIRECTORY_TABLE_BASE = self.USER_DATA + 5
2569 #USER_SID = self.USER_DATA + 6
2570 IMAGE_FILE_NAME = self.USER_DATA + 7
2571 COMMAND_LINE = self.USER_DATA + 8
2572
2573 ppid = line[self.PID]
2574 pid = int(line[PROCESS_ID], 16)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002575 command_line = CommandLineToArgvW(line[COMMAND_LINE])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002576 logging.debug(
2577 'New process %d->%d (%s) %s' %
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002578 (ppid, pid, line[IMAGE_FILE_NAME], command_line))
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002579
2580 if pid == self._thunk_pid:
2581 # Need to ignore processes we don't know about because the log is
2582 # system-wide. self._thunk_pid shall start only one process.
2583 # This is tricky though because Windows *loves* to reuse process id and
2584 # it happens often that the process ID of the thunk script created by
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002585 # create_subprocess_thunk() is reused. So just detecting the pid here is
2586 # not sufficient, we must confirm the command line.
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002587 if command_line[:len(self._thunk_cmd)] != self._thunk_cmd:
2588 logging.info(
2589 'Ignoring duplicate pid %d for %s: %s while searching for %s',
2590 pid, self._trace_name, command_line, self._thunk_cmd)
2591 return
2592
2593 # TODO(maruel): The check is quite weak. Add the thunk path.
2594 if self._thunk_process:
2595 raise TracingFailure(
2596 ( 'Parent process is _thunk_pid(%d) but thunk_process(%d) is '
2597 'already set') % (self._thunk_pid, self._thunk_process.pid),
2598 None, None, None)
2599 proc = self.Process(self.blacklist, pid, None)
2600 self._thunk_process = proc
2601 return
2602 elif ppid == self._thunk_pid and self._thunk_process:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002603 proc = self.Process(self.blacklist, pid, None)
2604 self.root_process = proc
2605 ppid = None
2606 elif self._process_lookup.get(ppid):
2607 proc = self.Process(self.blacklist, pid, None)
2608 self._process_lookup[ppid].children.append(proc)
2609 else:
2610 # Ignore
2611 return
2612 self._process_lookup[pid] = proc
2613
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002614 proc.command = command_line
2615 proc.executable = line[IMAGE_FILE_NAME]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002616 # proc.command[0] may be the absolute path of 'executable' but it may be
2617 # anything else too. If it happens that command[0] ends with executable,
2618 # use it, otherwise defaults to the base name.
2619 cmd0 = proc.command[0].lower()
2620 if not cmd0.endswith('.exe'):
2621 # TODO(maruel): That's not strictly true either.
2622 cmd0 += '.exe'
maruel12e30012015-10-09 11:55:35 -07002623 if cmd0.endswith(proc.executable) and fs.isfile(cmd0):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002624 # Fix the path.
2625 cmd0 = cmd0.replace('/', os.path.sep)
2626 cmd0 = os.path.normpath(cmd0)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002627 proc.executable = file_path.get_native_path_case(cmd0)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002628 logging.info(
2629 'New child: %s -> %d %s' % (ppid, pid, proc.executable))
2630
2631 def handle_Thread_End(self, line):
2632 """Has the same parameters as Thread_Start."""
2633 tid = int(line[self.TID], 16)
2634 self._threads_active.pop(tid, None)
2635
2636 def handle_Thread_Start(self, line):
2637 """Handles a new thread created.
2638
2639 Do not use self.PID here since a process' initial thread is created by
2640 the parent process.
2641 """
2642 PROCESS_ID = self.USER_DATA
2643 TTHREAD_ID = self.USER_DATA + 1
2644 #STACK_BASE = self.USER_DATA + 2
2645 #STACK_LIMIT = self.USER_DATA + 3
2646 #USER_STACK_BASE = self.USER_DATA + 4
2647 #USER_STACK_LIMIT = self.USER_DATA + 5
2648 #AFFINITY = self.USER_DATA + 6
2649 #WIN32_START_ADDR = self.USER_DATA + 7
2650 #TEB_BASE = self.USER_DATA + 8
2651 #SUB_PROCESS_TAG = self.USER_DATA + 9
2652 #BASE_PRIORITY = self.USER_DATA + 10
2653 #PAGE_PRIORITY = self.USER_DATA + 11
2654 #IO_PRIORITY = self.USER_DATA + 12
2655 #THREAD_FLAGS = self.USER_DATA + 13
2656 # Do not use self.PID here since a process' initial thread is created by
2657 # the parent process.
2658 pid = int(line[PROCESS_ID], 16)
2659 tid = int(line[TTHREAD_ID], 16)
2660 logging.debug('New thread pid:%d, tid:%d' % (pid, tid))
2661 self._threads_active[tid] = pid
2662
2663 @classmethod
2664 def supported_events(cls):
2665 """Returns all the procesed events."""
2666 out = []
2667 for member in dir(cls):
2668 match = re.match(r'^handle_([A-Za-z]+)_([A-Za-z]+)$', member)
2669 if match:
2670 out.append(match.groups())
2671 return out
2672
2673 class Tracer(ApiBase.Tracer):
2674 # The basic headers.
2675 EXPECTED_HEADER = [
2676 u'Event Name',
2677 u'Type',
2678 u'Event ID',
2679 u'Version',
2680 u'Channel',
2681 u'Level', # 5
2682 u'Opcode',
2683 u'Task',
2684 u'Keyword',
2685 u'PID',
2686 u'TID', # 10
2687 u'Processor Number',
2688 u'Instance ID',
2689 u'Parent Instance ID',
2690 u'Activity ID',
2691 u'Related Activity ID', # 15
2692 u'Clock-Time',
2693 u'Kernel(ms)', # Both have a resolution of ~15ms which makes them
2694 u'User(ms)', # pretty much useless.
2695 u'User Data', # Extra arguments that are event-specific.
2696 ]
2697 # Only the useful headers common to all entries are listed there. Any column
2698 # at 19 or higher is dependent on the specific event.
2699 EVENT_NAME = 0
2700 TYPE = 1
2701 PID = 9
2702 TID = 10
2703 PROCESSOR_ID = 11
2704 TIMESTAMP = 16
2705 NULL_GUID = '{00000000-0000-0000-0000-000000000000}'
2706 USER_DATA = 19
2707
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002708 class CsvReader(object):
2709 """CSV reader that reads files generated by tracerpt.exe.
2710
2711 csv.reader() fails to read them properly, it mangles file names quoted
2712 with "" with a comma in it.
2713 """
2714 # 0. Had a ',' or one of the following ' ' after a comma, next should
2715 # be ' ', '"' or string or ',' for an empty field.
2716 ( HAD_DELIMITER,
2717 # 1. Processing an unquoted field up to ','.
2718 IN_STR,
2719 # 2. Processing a new field starting with '"'.
2720 STARTING_STR_QUOTED,
2721 # 3. Second quote in a row at the start of a field. It could be either
2722 # '""foo""' or '""'. Who the hell thought it was a great idea to use
2723 # the same character for delimiting and escaping?
2724 STARTING_SECOND_QUOTE,
2725 # 4. A quote inside a quoted string where the previous character was
2726 # not a quote, so the string is not empty. Can be either: end of a
2727 # quoted string (a delimiter) or a quote escape. The next char must be
2728 # either '"' or ','.
2729 HAD_QUOTE_IN_QUOTED,
2730 # 5. Second quote inside a quoted string.
2731 HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED,
2732 # 6. Processing a field that started with '"'.
2733 IN_STR_QUOTED) = range(7)
2734
2735 def __init__(self, f):
2736 self.f = f
2737
2738 def __iter__(self):
2739 return self
2740
2741 def next(self):
2742 """Splits the line in fields."""
2743 line = self.f.readline()
2744 if not line:
2745 raise StopIteration()
2746 line = line.strip()
2747 fields = []
2748 state = self.HAD_DELIMITER
2749 for i, c in enumerate(line):
2750 if state == self.HAD_DELIMITER:
2751 if c == ',':
2752 # Empty field.
2753 fields.append('')
2754 elif c == ' ':
2755 # Ignore initial whitespaces
2756 pass
2757 elif c == '"':
2758 state = self.STARTING_STR_QUOTED
2759 fields.append('')
2760 else:
2761 # Start of a new field.
2762 state = self.IN_STR
2763 fields.append(c)
2764
2765 elif state == self.IN_STR:
2766 # Do not accept quote inside unquoted field.
2767 assert c != '"', (i, c, line, fields)
2768 if c == ',':
2769 fields[-1] = fields[-1].strip()
2770 state = self.HAD_DELIMITER
2771 else:
2772 fields[-1] = fields[-1] + c
2773
2774 elif state == self.STARTING_STR_QUOTED:
2775 if c == '"':
2776 # Do not store the character yet.
2777 state = self.STARTING_SECOND_QUOTE
2778 else:
2779 state = self.IN_STR_QUOTED
2780 fields[-1] = fields[-1] + c
2781
2782 elif state == self.STARTING_SECOND_QUOTE:
2783 if c == ',':
2784 # It was an empty field. '""' == ''.
2785 state = self.HAD_DELIMITER
2786 else:
2787 fields[-1] = fields[-1] + '"' + c
2788 state = self.IN_STR_QUOTED
2789
2790 elif state == self.HAD_QUOTE_IN_QUOTED:
2791 if c == ',':
2792 # End of the string.
2793 state = self.HAD_DELIMITER
2794 elif c == '"':
2795 state = self.HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED
2796 else:
2797 # The previous double-quote was just an unescaped quote.
2798 fields[-1] = fields[-1] + '"' + c
2799 state = self.IN_STR_QUOTED
2800
2801 elif state == self.HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED:
2802 if c == ',':
2803 # End of the string.
2804 state = self.HAD_DELIMITER
2805 fields[-1] = fields[-1] + '"'
2806 else:
maruel@chromium.org261d1352013-02-25 19:23:20 +00002807 # That's just how the logger rolls. Revert back to appending the
2808 # char and "guess" it was a quote in a double-quoted string.
2809 state = self.IN_STR_QUOTED
2810 fields[-1] = fields[-1] + '"' + c
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002811
2812 elif state == self.IN_STR_QUOTED:
2813 if c == '"':
2814 # Could be a delimiter or an escape.
2815 state = self.HAD_QUOTE_IN_QUOTED
2816 else:
2817 fields[-1] = fields[-1] + c
2818
2819 if state == self.HAD_SECOND_QUOTE_IN_A_ROW_IN_QUOTED:
2820 fields[-1] = fields[-1] + '"'
2821 else:
2822 assert state in (
2823 # Terminated with a normal field.
2824 self.IN_STR,
2825 # Terminated with an empty field.
2826 self.STARTING_SECOND_QUOTE,
2827 # Terminated with a normal quoted field.
2828 self.HAD_QUOTE_IN_QUOTED), (
2829 line, state, fields)
2830 return fields
2831
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002832 def __init__(self, logname):
2833 """Starts the log collection.
2834
2835 Requires administrative access. logman.exe is synchronous so no need for a
2836 "warmup" call. 'Windows Kernel Trace' is *localized* so use its GUID
2837 instead. The GUID constant name is SystemTraceControlGuid. Lovely.
2838
2839 One can get the list of potentially interesting providers with:
2840 "logman query providers | findstr /i file"
2841 """
2842 super(LogmanTrace.Tracer, self).__init__(logname)
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002843 self._signal_script = create_subprocess_thunk()
2844 self._scripts_to_cleanup.append(self._signal_script)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002845 cmd_start = [
2846 'logman.exe',
2847 'start',
2848 'NT Kernel Logger',
2849 '-p', '{9e814aad-3204-11d2-9a82-006008a86939}',
2850 # splitio,fileiocompletion,syscall,file,cswitch,img
2851 '(process,fileio,thread)',
2852 '-o', self._logname + '.etl',
2853 '-ets', # Send directly to kernel
2854 # Values extracted out of thin air.
2855 # Event Trace Session buffer size in kb.
2856 '-bs', '10240',
2857 # Number of Event Trace Session buffers.
2858 '-nb', '16', '256',
2859 ]
2860 logging.debug('Running: %s' % cmd_start)
2861 try:
2862 subprocess.check_call(
2863 cmd_start,
2864 stdin=subprocess.PIPE,
2865 stdout=subprocess.PIPE,
2866 stderr=subprocess.STDOUT)
2867 except subprocess.CalledProcessError, e:
2868 if e.returncode == -2147024891:
2869 print >> sys.stderr, 'Please restart with an elevated admin prompt'
2870 elif e.returncode == -2144337737:
2871 print >> sys.stderr, (
2872 'A kernel trace was already running, stop it and try again')
2873 raise
2874
2875 def trace(self, cmd, cwd, tracename, output):
2876 logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
2877 assert os.path.isabs(cmd[0]), cmd[0]
2878 assert os.path.isabs(cwd), cwd
2879 assert os.path.normpath(cwd) == cwd, cwd
2880 with self._lock:
2881 if not self._initialized:
2882 raise TracingFailure(
2883 'Called Tracer.trace() on an unitialized object',
2884 None, None, None, tracename)
2885 assert tracename not in (i['trace'] for i in self._traces)
2886
2887 # Use "logman -?" for help.
2888
2889 stdout = stderr = None
2890 if output:
2891 stdout = subprocess.PIPE
2892 stderr = subprocess.STDOUT
2893
2894 # Run the child process.
2895 logging.debug('Running: %s' % cmd)
maruel@chromium.org2909b7c2013-05-29 18:52:59 +00002896 # Use the temporary script generated with create_subprocess_thunk() so we
2897 # have a clear pid owner. Since trace_inputs.py can be used as a library
2898 # and could trace multiple processes simultaneously, it makes it more
2899 # complex if the executable to be traced is executed directly here. It
2900 # also solves issues related to logman.exe that needs to be executed to
2901 # control the kernel trace.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002902 child_cmd = [
2903 sys.executable,
maruel@chromium.org4c9b4fa2013-05-29 19:22:14 +00002904 self._signal_script,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002905 tracename,
2906 ]
2907 child = subprocess.Popen(
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002908 child_cmd + tools.fix_python_path(cmd),
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002909 cwd=cwd,
2910 stdin=subprocess.PIPE,
2911 stdout=stdout,
2912 stderr=stderr)
2913 logging.debug('Started child pid: %d' % child.pid)
2914 out = child.communicate()[0]
2915 # This doesn't mean all the grand-children are done. Sadly, we don't have
2916 # a good way to determine that.
2917
2918 with self._lock:
2919 assert tracename not in (i['trace'] for i in self._traces)
2920 self._traces.append({
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00002921 'cmd': cmd,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002922 'cwd': cwd,
maruel@chromium.org964ddde2012-10-29 18:13:46 +00002923 'output': out,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002924 'pid': child.pid,
maruel@chromium.org19e9fcd2012-10-29 18:24:02 +00002925 # Used to figure out the real process when process ids are reused.
2926 'thunk_cmd': child_cmd,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002927 'trace': tracename,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002928 })
2929
2930 return child.returncode, out
2931
2932 def close(self, _timeout=None):
2933 """Stops the kernel log collection and converts the traces to text
2934 representation.
2935 """
2936 with self._lock:
maruel@chromium.org808e3f22013-05-16 16:42:54 +00002937 try:
2938 super(LogmanTrace.Tracer, self).close()
2939 finally:
2940 cmd_stop = [
2941 'logman.exe',
2942 'stop',
2943 'NT Kernel Logger',
2944 '-ets', # Sends the command directly to the kernel.
2945 ]
2946 logging.debug('Running: %s' % cmd_stop)
2947 subprocess.check_call(
2948 cmd_stop,
2949 stdin=subprocess.PIPE,
2950 stdout=subprocess.PIPE,
2951 stderr=subprocess.STDOUT)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002952
2953 def post_process_log(self):
2954 """Converts the .etl file into .csv then into .json."""
2955 super(LogmanTrace.Tracer, self).post_process_log()
2956 logformat = 'csv'
2957 self._convert_log(logformat)
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00002958 self._trim_log(logformat)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002959
maruel@chromium.org808e3f22013-05-16 16:42:54 +00002960 def _gen_logdata(self):
2961 return {
2962 'format': 'csv',
2963 'traces': self._traces,
2964 }
2965
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00002966 def _trim_log(self, logformat):
2967 """Reduces the amount of data in original log by generating a 'reduced'
2968 log.
2969 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002970 if logformat == 'csv_utf16':
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002971 file_handle = codecs.open(
2972 self._logname + '.' + logformat, 'r', encoding='utf-16')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002973
2974 elif logformat == 'csv':
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002975 assert sys.getfilesystemencoding() == 'mbcs'
2976 file_handle = codecs.open(
2977 self._logname + '.' + logformat, 'r',
2978 encoding=get_current_encoding())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002979
2980 supported_events = LogmanTrace.Context.supported_events()
2981
2982 def trim(generator):
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00002983 """Loads items from the generator and returns the interesting data.
2984
2985 It filters out any uninteresting line and reduce the amount of data in
2986 the trace.
2987 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002988 for index, line in enumerate(generator):
2989 if not index:
2990 if line != self.EXPECTED_HEADER:
2991 raise TracingFailure(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00002992 'Found malformed header: %s' % line,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002993 None, None, None)
2994 continue
2995 # As you can see, the CSV is full of useful non-redundant information:
2996 if (line[2] != '0' or # Event ID
2997 line[3] not in ('2', '3') or # Version
2998 line[4] != '0' or # Channel
2999 line[5] != '0' or # Level
3000 line[7] != '0' or # Task
3001 line[8] != '0x0000000000000000' or # Keyword
3002 line[12] != '' or # Instance ID
3003 line[13] != '' or # Parent Instance ID
3004 line[14] != self.NULL_GUID or # Activity ID
3005 line[15] != ''): # Related Activity ID
3006 raise TracingFailure(
3007 'Found unexpected values in line: %s' % ' '.join(line),
3008 None, None, None)
3009
3010 if (line[self.EVENT_NAME], line[self.TYPE]) not in supported_events:
3011 continue
3012
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003013 yield [
3014 line[self.EVENT_NAME],
3015 line[self.TYPE],
3016 line[self.PID],
3017 line[self.TID],
3018 line[self.PROCESSOR_ID],
3019 line[self.TIMESTAMP],
3020 ] + line[self.USER_DATA:]
3021
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003022 # must not convert the trim() call into a list, since it will use too much
3023 # memory for large trace. use a csv file as a workaround since the json
3024 # parser requires a complete in-memory file.
maruel12e30012015-10-09 11:55:35 -07003025 path = os.path.join(
3026 unicode(os.getcwd()), u'%s.preprocessed' % self._logname)
3027 with fs.open(path, 'wb') as f:
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003028 # $ and * can't be used in file name on windows, reducing the likelihood
3029 # of having to escape a string.
3030 out = csv.writer(
3031 f, delimiter='$', quotechar='*', quoting=csv.QUOTE_MINIMAL)
maruel@chromium.org306e0e72012-11-02 18:22:03 +00003032 for line in trim(self.CsvReader(file_handle)):
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003033 out.writerow([s.encode('utf-8') for s in line])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003034
3035 def _convert_log(self, logformat):
3036 """Converts the ETL trace to text representation.
3037
3038 Normally, 'csv' is sufficient. If complex scripts are used (like eastern
3039 languages), use 'csv_utf16'. If localization gets in the way, use 'xml'.
3040
3041 Arguments:
3042 - logformat: Text format to be generated, csv, csv_utf16 or xml.
3043
3044 Use "tracerpt -?" for help.
3045 """
3046 LOCALE_INVARIANT = 0x7F
3047 windll.kernel32.SetThreadLocale(LOCALE_INVARIANT)
3048 cmd_convert = [
3049 'tracerpt.exe',
3050 '-l', self._logname + '.etl',
3051 '-o', self._logname + '.' + logformat,
3052 '-gmt', # Use UTC
3053 '-y', # No prompt
3054 # Use -of XML to get the header of each items after column 19, e.g. all
3055 # the actual headers of 'User Data'.
3056 ]
3057
3058 if logformat == 'csv':
3059 # tracerpt localizes the 'Type' column, for major brainfuck
3060 # entertainment. I can't imagine any sane reason to do that.
3061 cmd_convert.extend(['-of', 'CSV'])
3062 elif logformat == 'csv_utf16':
3063 # This causes it to use UTF-16, which doubles the log size but ensures
3064 # the log is readable for non-ASCII characters.
3065 cmd_convert.extend(['-of', 'CSV', '-en', 'Unicode'])
3066 elif logformat == 'xml':
3067 cmd_convert.extend(['-of', 'XML'])
3068 else:
3069 raise ValueError('Unexpected log format \'%s\'' % logformat)
3070 logging.debug('Running: %s' % cmd_convert)
3071 # This can takes tens of minutes for large logs.
3072 # Redirects all output to stderr.
3073 subprocess.check_call(
3074 cmd_convert,
3075 stdin=subprocess.PIPE,
3076 stdout=sys.stderr,
3077 stderr=sys.stderr)
3078
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003079 def __init__(self, use_sudo=False): # pylint: disable=W0613
3080 super(LogmanTrace, self).__init__()
3081 # Ignore use_sudo. It's irrelevant on Windows but kept to simplify the API.
3082
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003083 @staticmethod
3084 def clean_trace(logname):
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003085 for ext in ('', '.csv', '.etl', '.json', '.xml', '.preprocessed'):
maruel12e30012015-10-09 11:55:35 -07003086 if fs.isfile(logname + ext):
3087 fs.remove(logname + ext)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003088
3089 @classmethod
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003090 def parse_log(cls, logname, blacklist, trace_name):
3091 logging.info('parse_log(%s, ..., %s)', logname, trace_name)
maruel@chromium.org8b056ba2012-10-16 14:04:49 +00003092 assert os.path.isabs(logname)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003093
3094 def blacklist_more(filepath):
3095 # All the NTFS metadata is in the form x:\$EXTEND or stuff like that.
3096 return blacklist(filepath) or re.match(r'[A-Z]\:\\\$EXTEND', filepath)
3097
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003098 # Create a list of (Context, result_dict) tuples. This is necessary because
3099 # the csv file may be larger than the amount of available memory.
3100 contexes = [
3101 (
3102 cls.Context(
3103 blacklist_more, item['pid'], item['trace'], item['thunk_cmd']),
3104 {
3105 'output': item['output'],
3106 'trace': item['trace'],
3107 },
3108 )
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05003109 for item in tools.read_json(logname)['traces']
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003110 if not trace_name or item['trace'] == trace_name
3111 ]
3112
3113 # The log may be too large to fit in memory and it is not efficient to read
3114 # it multiple times, so multiplex the contexes instead, which is slightly
3115 # more awkward.
maruel12e30012015-10-09 11:55:35 -07003116 path = os.path.join(unicode(os.getcwd()), u'%s.preprocessed' % logname)
3117 with fs.open(path, 'rb') as f:
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003118 lines = csv.reader(
3119 f, delimiter='$', quotechar='*', quoting=csv.QUOTE_MINIMAL)
3120 for encoded in lines:
3121 line = [s.decode('utf-8') for s in encoded]
3122 # Convert the PID in-place from hex.
3123 line[cls.Context.PID] = int(line[cls.Context.PID], 16)
3124 for context in contexes:
3125 if 'exception' in context[1]:
3126 continue
3127 try:
3128 context[0].on_line(line)
3129 except TracingFailure:
3130 context[1]['exception'] = sys.exc_info()
3131
3132 for context in contexes:
3133 if 'exception' in context[1]:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003134 continue
maruel@chromium.orgeaedeb42012-10-31 14:38:22 +00003135 context[1]['results'] = context[0].to_results()
3136
3137 return [context[1] for context in contexes]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003138
3139
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003140def get_api(**kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003141 """Returns the correct implementation for the current OS."""
3142 if sys.platform == 'cygwin':
3143 raise NotImplementedError(
3144 'Not implemented for cygwin, start the script from Win32 python')
3145 flavors = {
3146 'win32': LogmanTrace,
3147 'darwin': Dtrace,
3148 'sunos5': Dtrace,
3149 'freebsd7': Dtrace,
3150 'freebsd8': Dtrace,
3151 }
3152 # Defaults to strace.
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003153 return flavors.get(sys.platform, Strace)(**kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003154
3155
3156def extract_directories(root_dir, files, blacklist):
3157 """Detects if all the files in a directory are in |files| and if so, replace
3158 the individual files by a Results.Directory instance.
3159
3160 Takes a list of Results.File instances and returns a shorter list of
3161 Results.File and Results.Directory instances.
3162
3163 Arguments:
3164 - root_dir: Optional base directory that shouldn't be search further.
3165 - files: list of Results.File instances.
maruel@chromium.org3683afe2013-07-27 00:09:27 +00003166 - blacklist: lambda to reject unneeded files, for example r'.+\.pyc'.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003167 """
3168 logging.info(
3169 'extract_directories(%s, %d files, ...)' % (root_dir, len(files)))
3170 assert not (root_dir or '').endswith(os.path.sep), root_dir
maruel@chromium.org306e0e72012-11-02 18:22:03 +00003171 # It is important for root_dir to not be a symlinked path, make sure to call
3172 # os.path.realpath() as needed.
3173 assert not root_dir or (
maruel12e30012015-10-09 11:55:35 -07003174 fs.realpath(file_path.get_native_path_case(root_dir)) == root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003175 assert not any(isinstance(f, Results.Directory) for f in files)
3176 # Remove non existent files.
3177 files = [f for f in files if f.existent]
3178 if not files:
3179 return files
3180 # All files must share the same root, which can be None.
3181 assert len(set(f.root for f in files)) == 1, set(f.root for f in files)
3182
3183 # Creates a {directory: {filename: File}} mapping, up to root.
3184 buckets = {}
3185 if root_dir:
3186 buckets[root_dir] = {}
3187 for fileobj in files:
3188 path = fileobj.full_path
3189 directory = os.path.dirname(path)
3190 assert directory
3191 # Do not use os.path.basename() so trailing os.path.sep is kept.
3192 basename = path[len(directory)+1:]
3193 files_in_directory = buckets.setdefault(directory, {})
3194 files_in_directory[basename] = fileobj
3195 # Add all the directories recursively up to root.
3196 while True:
3197 old_d = directory
3198 directory = os.path.dirname(directory)
3199 if directory + os.path.sep == root_dir or directory == old_d:
3200 break
3201 buckets.setdefault(directory, {})
3202
3203 root_prefix = len(root_dir) + 1 if root_dir else 0
3204 for directory in sorted(buckets, reverse=True):
maruel12e30012015-10-09 11:55:35 -07003205 if not fs.isdir(directory):
maruel@chromium.orgb1678e62013-03-19 15:55:17 +00003206 logging.debug(
3207 '%s was a directory but doesn\'t exist anymore; ignoring', directory)
3208 continue
maruel12e30012015-10-09 11:55:35 -07003209 actual = set(f for f in fs.listdir(directory) if not blacklist(f))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003210 expected = set(buckets[directory])
3211 if not (actual - expected):
3212 parent = os.path.dirname(directory)
3213 buckets[parent][os.path.basename(directory)] = Results.Directory(
3214 root_dir,
3215 directory[root_prefix:],
3216 False,
3217 sum(f.size for f in buckets[directory].itervalues()),
3218 sum(f.nb_files for f in buckets[directory].itervalues()))
3219 # Remove the whole bucket.
3220 del buckets[directory]
3221
3222 # Reverse the mapping with what remains. The original instances are returned,
3223 # so the cached meta data is kept.
3224 files = sum((x.values() for x in buckets.itervalues()), [])
3225 return sorted(files, key=lambda x: x.path)
3226
3227
3228def trace(logfile, cmd, cwd, api, output):
3229 """Traces an executable. Returns (returncode, output) from api.
3230
3231 Arguments:
3232 - logfile: file to write to.
3233 - cmd: command to run.
3234 - cwd: current directory to start the process in.
3235 - api: a tracing api instance.
3236 - output: if True, returns output, otherwise prints it at the console.
3237 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003238 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003239 api.clean_trace(logfile)
3240 with api.get_tracer(logfile) as tracer:
3241 return tracer.trace(cmd, cwd, 'default', output)
3242
3243
maruel@chromium.orge5322512013-08-19 20:17:57 +00003244def CMDclean(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003245 """Cleans up traces."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003246 options, args = parser.parse_args(args)
3247 api = get_api()
3248 api.clean_trace(options.log)
3249 return 0
3250
3251
maruel@chromium.orge5322512013-08-19 20:17:57 +00003252def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003253 """Traces an executable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003254 parser.allow_interspersed_args = False
3255 parser.add_option(
3256 '-q', '--quiet', action='store_true',
3257 help='Redirects traced executable output to /dev/null')
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003258 parser.add_option(
3259 '-s', '--sudo', action='store_true',
3260 help='Use sudo when shelling out the tracer tool (ignored on Windows)')
3261 parser.add_option(
3262 '-n', '--no-sudo', action='store_false',
3263 help='Don\'t use sudo')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003264 options, args = parser.parse_args(args)
3265
3266 if not args:
3267 parser.error('Please provide a command to run')
3268
Marc-Antoine Ruel5906fcd2013-11-08 21:02:24 -05003269 if not can_trace():
3270 parser.error('Please rerun this program with admin privileges')
3271
maruel12e30012015-10-09 11:55:35 -07003272 if not os.path.isabs(args[0]) and fs.access(args[0], os.X_OK):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003273 args[0] = os.path.abspath(args[0])
3274
maruel@chromium.orgf14633b2013-01-25 15:12:52 +00003275 # options.sudo default value is None, which is to do whatever tracer defaults
3276 # do.
3277 api = get_api(use_sudo=options.sudo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003278 return trace(options.log, args, os.getcwd(), api, options.quiet)[0]
3279
3280
maruel@chromium.orge5322512013-08-19 20:17:57 +00003281def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003282 """Reads the logs and prints the result."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003283 parser.add_option(
3284 '-V', '--variable',
3285 nargs=2,
3286 action='append',
3287 dest='variables',
3288 metavar='VAR_NAME directory',
3289 default=[],
3290 help=('Variables to replace relative directories against. Example: '
3291 '"-v \'$HOME\' \'/home/%s\'" will replace all occurence of your '
3292 'home dir with $HOME') % getpass.getuser())
3293 parser.add_option(
3294 '--root-dir',
3295 help='Root directory to base everything off it. Anything outside of this '
3296 'this directory will not be reported')
3297 parser.add_option(
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003298 '--trace-name',
3299 help='Only reads one of the trace. Defaults to reading all traces')
3300 parser.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003301 '-j', '--json', action='store_true',
3302 help='Outputs raw result data as json')
3303 parser.add_option(
maruel@chromium.org3683afe2013-07-27 00:09:27 +00003304 '--trace-blacklist', action='append', default=[],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003305 help='List of regexp to use as blacklist filter')
3306 options, args = parser.parse_args(args)
3307
3308 if options.root_dir:
maruel@chromium.org561d4b22013-09-26 21:08:08 +00003309 options.root_dir = file_path.get_native_path_case(
maruel@chromium.org7dc17b32013-04-02 22:27:05 +00003310 unicode(os.path.abspath(options.root_dir)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003311
3312 variables = dict(options.variables)
3313 api = get_api()
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05003314 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00003315 data = api.parse_log(options.log, blacklist, options.trace_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003316 # Process each trace.
3317 output_as_json = []
maruel@chromium.org9e56b592013-05-21 16:56:05 +00003318 try:
3319 for item in data:
3320 if 'exception' in item:
3321 # Do not abort the other traces.
3322 print >> sys.stderr, (
3323 'Trace %s: Got an exception: %s' % (
3324 item['trace'], item['exception'][1]))
3325 continue
3326 results = item['results']
3327 if options.root_dir:
3328 results = results.strip_root(options.root_dir)
3329
3330 if options.json:
3331 output_as_json.append(results.flatten())
3332 else:
3333 simplified = extract_directories(
3334 options.root_dir, results.files, blacklist)
3335 simplified = [f.replace_variables(variables) for f in simplified]
3336 if len(data) > 1:
3337 print('Trace: %s' % item['trace'])
3338 print('Total: %d' % len(results.files))
3339 print('Non existent: %d' % len(results.non_existent))
3340 for f in results.non_existent:
3341 print(' %s' % f.path)
3342 print(
3343 'Interesting: %d reduced to %d' % (
3344 len(results.existent), len(simplified)))
3345 for f in simplified:
3346 print(' %s' % f.path)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003347
3348 if options.json:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05003349 tools.write_json(sys.stdout, output_as_json, False)
maruel@chromium.org9e56b592013-05-21 16:56:05 +00003350 except KeyboardInterrupt:
3351 return 1
3352 except IOError as e:
3353 if e.errno == errno.EPIPE:
3354 # Do not print a stack trace when the output is piped to less and the user
3355 # quits before the whole output was written.
3356 return 1
3357 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003358 return 0
3359
3360
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04003361class OptionParserTraceInputs(logging_utils.OptionParserWithLogging):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003362 """Adds automatic --log handling."""
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003363
3364 # Disable --log-file options since both --log and --log-file options are
3365 # confusing.
3366 # TODO(vadimsh): Rename --log-file or --log to something else.
3367 enable_log_file = False
3368
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003369 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04003370 logging_utils.OptionParserWithLogging.__init__(self, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003371 self.add_option(
3372 '-l', '--log', help='Log file to generate or read, required')
3373
3374 def parse_args(self, *args, **kwargs):
3375 """Makes sure the paths make sense.
3376
3377 On Windows, / and \ are often mixed together in a path.
3378 """
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04003379 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003380 self, *args, **kwargs)
3381 if not options.log:
3382 self.error('Must supply a log file with -l')
maruel12e30012015-10-09 11:55:35 -07003383 options.log = unicode(os.path.abspath(options.log))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003384 return options, args
3385
3386
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003387def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00003388 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003389 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00003390 return dispatcher.execute(
3391 OptionParserTraceInputs(version=__version__), argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003392 except TracingFailure, e:
3393 sys.stderr.write('\nError: ')
3394 sys.stderr.write(str(e))
3395 sys.stderr.write('\n')
3396 return 1
3397
3398
3399if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00003400 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00003401 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00003402 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00003403 sys.exit(main(sys.argv[1:]))