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