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