aboutsummaryrefslogtreecommitdiff
path: root/extras/stracedeps.py
blob: 37d82cbb724b14f4068415f2d49d3e9f60df20a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#!/usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2015 (ita)

"""
Execute tasks through strace to obtain dependencies after the process is run. This
scheme is similar to that of the Fabricate script.

To use::

  def configure(conf):
     conf.load('strace')

WARNING:
* This will not work when advanced scanners are needed (qt4/qt5)
* The overhead of running 'strace' is significant (56s -> 1m29s)
* It will not work on Windows :-)
"""

import os, re, threading
from waflib import Task, Logs, Utils

#TRACECALLS = 'trace=access,chdir,clone,creat,execve,exit_group,fork,lstat,lstat64,mkdir,open,rename,stat,stat64,symlink,vfork'
TRACECALLS = 'trace=process,file'

BANNED = ('/tmp', '/proc', '/sys', '/dev')

s_process = r'(?:clone|fork|vfork)\(.*?(?P<npid>\d+)'
s_file = r'(?P<call>\w+)\("(?P<path>([^"\\]|\\.)*)"(.*)'
re_lines = re.compile(r'^(?P<pid>\d+)\s+(?:(?:%s)|(?:%s))\r*$' % (s_file, s_process), re.IGNORECASE | re.MULTILINE)
strace_lock = threading.Lock()

def configure(conf):
	conf.find_program('strace')

def task_method(func):
	# Decorator function to bind/replace methods on the base Task class
	#
	# The methods Task.exec_command and Task.sig_implicit_deps already exists and are rarely overridden
	# we thus expect that we are the only ones doing this
	try:
		setattr(Task.Task, 'nostrace_%s' % func.__name__, getattr(Task.Task, func.__name__))
	except AttributeError:
		pass
	setattr(Task.Task, func.__name__, func)
	return func

@task_method
def get_strace_file(self):
	try:
		return self.strace_file
	except AttributeError:
		pass

	if self.outputs:
		ret = self.outputs[0].abspath() + '.strace'
	else:
		ret = '%s%s%d%s' % (self.generator.bld.bldnode.abspath(), os.sep, id(self), '.strace')
	self.strace_file = ret
	return ret

@task_method
def get_strace_args(self):
	return (self.env.STRACE or ['strace']) + ['-e', TRACECALLS, '-f', '-o', self.get_strace_file()]

@task_method
def exec_command(self, cmd, **kw):
	bld = self.generator.bld
	if not 'cwd' in kw:
		kw['cwd'] = self.get_cwd()

	args = self.get_strace_args()
	fname = self.get_strace_file()
	if isinstance(cmd, list):
		cmd = args + cmd
	else:
		cmd = '%s %s' % (' '.join(args), cmd)

	try:
		ret = bld.exec_command(cmd, **kw)
	finally:
		if not ret:
			self.parse_strace_deps(fname, kw['cwd'])
	return ret

@task_method
def sig_implicit_deps(self):
	# bypass the scanner functions
	return

@task_method
def parse_strace_deps(self, path, cwd):
	# uncomment the following line to disable the dependencies and force a file scan
	# return
	try:
		cnt = Utils.readf(path)
	finally:
		try:
			os.remove(path)
		except OSError:
			pass

	if not isinstance(cwd, str):
		cwd = cwd.abspath()

	nodes = []
	bld = self.generator.bld
	try:
		cache = bld.strace_cache
	except AttributeError:
		cache = bld.strace_cache = {}

	# chdir and relative paths
	pid_to_cwd = {}

	global BANNED
	done = set()
	for m in re.finditer(re_lines, cnt):
		# scraping the output of strace
		pid = m.group('pid')
		if m.group('npid'):
			npid = m.group('npid')
			pid_to_cwd[npid] = pid_to_cwd.get(pid, cwd)
			continue

		p = m.group('path').replace('\\"', '"')

		if p == '.' or m.group().find('= -1 ENOENT') > -1:
			# just to speed it up a bit
			continue

		if not os.path.isabs(p):
			p = os.path.join(pid_to_cwd.get(pid, cwd), p)

		call = m.group('call')
		if call == 'chdir':
			pid_to_cwd[pid] = p
			continue

		if p in done:
			continue
		done.add(p)

		for x in BANNED:
			if p.startswith(x):
				break
		else:
			if p.endswith('/') or os.path.isdir(p):
				continue

			try:
				node = cache[p]
			except KeyError:
				strace_lock.acquire()
				try:
					cache[p] = node = bld.root.find_node(p)
					if not node:
						continue
				finally:
					strace_lock.release()
			nodes.append(node)

	# record the dependencies then force the task signature recalculation for next time
	if Logs.verbose:
		Logs.debug('deps: real scanner for %r returned %r', self, nodes)
	bld = self.generator.bld
	bld.node_deps[self.uid()] = nodes
	bld.raw_deps[self.uid()] = []
	try:
		del self.cache_sig
	except AttributeError:
		pass
	self.signature()