stracedeps.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2015 (ita)
  4. """
  5. Execute tasks through strace to obtain dependencies after the process is run. This
  6. scheme is similar to that of the Fabricate script.
  7. To use::
  8. def configure(conf):
  9. conf.load('strace')
  10. WARNING:
  11. * This will not work when advanced scanners are needed (qt4/qt5)
  12. * The overhead of running 'strace' is significant (56s -> 1m29s)
  13. * It will not work on Windows :-)
  14. """
  15. import os, re, threading
  16. from waflib import Task, Logs, Utils
  17. #TRACECALLS = 'trace=access,chdir,clone,creat,execve,exit_group,fork,lstat,lstat64,mkdir,open,rename,stat,stat64,symlink,vfork'
  18. TRACECALLS = 'trace=process,file'
  19. BANNED = ('/tmp', '/proc', '/sys', '/dev')
  20. s_process = r'(?:clone|fork|vfork)\(.*?(?P<npid>\d+)'
  21. s_file = r'(?P<call>\w+)\("(?P<path>([^"\\]|\\.)*)"(.*)'
  22. re_lines = re.compile(r'^(?P<pid>\d+)\s+(?:(?:%s)|(?:%s))\r*$' % (s_file, s_process), re.IGNORECASE | re.MULTILINE)
  23. strace_lock = threading.Lock()
  24. def configure(conf):
  25. conf.find_program('strace')
  26. def task_method(func):
  27. # Decorator function to bind/replace methods on the base Task class
  28. #
  29. # The methods Task.exec_command and Task.sig_implicit_deps already exists and are rarely overridden
  30. # we thus expect that we are the only ones doing this
  31. try:
  32. setattr(Task.Task, 'nostrace_%s' % func.__name__, getattr(Task.Task, func.__name__))
  33. except AttributeError:
  34. pass
  35. setattr(Task.Task, func.__name__, func)
  36. return func
  37. @task_method
  38. def get_strace_file(self):
  39. try:
  40. return self.strace_file
  41. except AttributeError:
  42. pass
  43. if self.outputs:
  44. ret = self.outputs[0].abspath() + '.strace'
  45. else:
  46. ret = '%s%s%d%s' % (self.generator.bld.bldnode.abspath(), os.sep, id(self), '.strace')
  47. self.strace_file = ret
  48. return ret
  49. @task_method
  50. def get_strace_args(self):
  51. return (self.env.STRACE or ['strace']) + ['-e', TRACECALLS, '-f', '-o', self.get_strace_file()]
  52. @task_method
  53. def exec_command(self, cmd, **kw):
  54. bld = self.generator.bld
  55. if not 'cwd' in kw:
  56. kw['cwd'] = self.get_cwd()
  57. args = self.get_strace_args()
  58. fname = self.get_strace_file()
  59. if isinstance(cmd, list):
  60. cmd = args + cmd
  61. else:
  62. cmd = '%s %s' % (' '.join(args), cmd)
  63. try:
  64. ret = bld.exec_command(cmd, **kw)
  65. finally:
  66. if not ret:
  67. self.parse_strace_deps(fname, kw['cwd'])
  68. return ret
  69. @task_method
  70. def sig_implicit_deps(self):
  71. # bypass the scanner functions
  72. return
  73. @task_method
  74. def parse_strace_deps(self, path, cwd):
  75. # uncomment the following line to disable the dependencies and force a file scan
  76. # return
  77. try:
  78. cnt = Utils.readf(path)
  79. finally:
  80. try:
  81. os.remove(path)
  82. except OSError:
  83. pass
  84. if not isinstance(cwd, str):
  85. cwd = cwd.abspath()
  86. nodes = []
  87. bld = self.generator.bld
  88. try:
  89. cache = bld.strace_cache
  90. except AttributeError:
  91. cache = bld.strace_cache = {}
  92. # chdir and relative paths
  93. pid_to_cwd = {}
  94. global BANNED
  95. done = set()
  96. for m in re.finditer(re_lines, cnt):
  97. # scraping the output of strace
  98. pid = m.group('pid')
  99. if m.group('npid'):
  100. npid = m.group('npid')
  101. pid_to_cwd[npid] = pid_to_cwd.get(pid, cwd)
  102. continue
  103. p = m.group('path').replace('\\"', '"')
  104. if p == '.' or m.group().find('= -1 ENOENT') > -1:
  105. # just to speed it up a bit
  106. continue
  107. if not os.path.isabs(p):
  108. p = os.path.join(pid_to_cwd.get(pid, cwd), p)
  109. call = m.group('call')
  110. if call == 'chdir':
  111. pid_to_cwd[pid] = p
  112. continue
  113. if p in done:
  114. continue
  115. done.add(p)
  116. for x in BANNED:
  117. if p.startswith(x):
  118. break
  119. else:
  120. if p.endswith('/') or os.path.isdir(p):
  121. continue
  122. try:
  123. node = cache[p]
  124. except KeyError:
  125. strace_lock.acquire()
  126. try:
  127. cache[p] = node = bld.root.find_node(p)
  128. if not node:
  129. continue
  130. finally:
  131. strace_lock.release()
  132. nodes.append(node)
  133. # record the dependencies then force the task signature recalculation for next time
  134. if Logs.verbose:
  135. Logs.debug('deps: real scanner for %r returned %r', self, nodes)
  136. bld = self.generator.bld
  137. bld.node_deps[self.uid()] = nodes
  138. bld.raw_deps[self.uid()] = []
  139. try:
  140. del self.cache_sig
  141. except AttributeError:
  142. pass
  143. self.signature()