|
dvander@86
|
1 |
# vim: set ts=8 sts=2 sw=2 tw=99 et: |
|
dvander@94
|
2 |
import errno |
|
dvander@117
|
3 |
import shutil |
|
dvander@94
|
4 |
import os, sys |
|
dvander@94
|
5 |
import traceback |
|
dvander@87
|
6 |
import multiprocessing as mp |
|
dvander@167
|
7 |
from ambuild2 import util |
|
dvander@167
|
8 |
from ambuild2 import nodetypes |
|
dvander@167
|
9 |
from ambuild2.ipc import ParentProcessListener, ChildProcessListener |
|
dvander@167
|
10 |
from ambuild2.ipc import ProcessManager, MessageListener, Error |
|
dvander@167
|
11 |
from ambuild2.ipc import Channel |
|
dvander@86
|
12 |
|
|
dvander@86
|
13 |
class Task(object): |
|
dvander@86
|
14 |
def __init__(self, id, entry, outputs): |
|
dvander@86
|
15 |
self.id = id |
|
dvander@86
|
16 |
self.type = entry.type |
|
dvander@86
|
17 |
self.data = entry.blob |
|
dvander@108
|
18 |
if entry.folder: |
|
dvander@108
|
19 |
self.folder = entry.folder.path |
|
dvander@108
|
20 |
else: |
|
dvander@108
|
21 |
self.folder = None |
|
dvander@86
|
22 |
self.outputs = outputs |
|
dvander@86
|
23 |
self.outgoing = [] |
|
dvander@86
|
24 |
self.incoming = set() |
|
dvander@86
|
25 |
|
|
dvander@86
|
26 |
def addOutgoing(self, task): |
|
dvander@86
|
27 |
self.outgoing.append(task) |
|
dvander@86
|
28 |
task.incoming.add(self) |
|
dvander@86
|
29 |
|
|
dvander@117
|
30 |
@property |
|
dvander@117
|
31 |
def folder_name(self): |
|
dvander@117
|
32 |
if not self.folder: |
|
dvander@117
|
33 |
return '' |
|
dvander@117
|
34 |
return self.folder |
|
dvander@117
|
35 |
|
|
dvander@86
|
36 |
def format(self): |
|
dvander@86
|
37 |
text = '' |
|
dvander@86
|
38 |
if self.type == nodetypes.Cxx: |
|
dvander@86
|
39 |
return '[' + self.data['type'] + ']' + ' -> ' + (' '.join([arg for arg in self.data['argv']])) |
|
dvander@115
|
40 |
if self.type == nodetypes.Symlink: |
|
dvander@117
|
41 |
return 'ln -s "{0}" "{1}"'.format(self.data[0], os.path.join(self.folder_name, self.data[1])) |
|
dvander@117
|
42 |
if self.type == nodetypes.Copy: |
|
dvander@117
|
43 |
return 'cp "{0}" "{1}"'.format(self.data[0], os.path.join(self.folder_name, self.data[1])) |
|
dvander@86
|
44 |
return (' '.join([arg for arg in self.data])) |
|
dvander@86
|
45 |
|
|
dvander@94
|
46 |
class WorkerChild(ChildProcessListener): |
|
dvander@142
|
47 |
def __init__(self, pump, channel, buildPath): |
|
dvander@131
|
48 |
super(WorkerChild, self).__init__(pump, channel) |
|
dvander@94
|
49 |
self.buildPath = buildPath |
|
dvander@94
|
50 |
self.pid = os.getpid() |
|
dvander@93
|
51 |
self.messageMap = { |
|
dvander@93
|
52 |
'task': lambda channel, message: self.receiveTask(channel, message) |
|
dvander@93
|
53 |
} |
|
dvander@94
|
54 |
self.taskMap = { |
|
dvander@94
|
55 |
'cxx': lambda message: self.doCompile(message), |
|
dvander@115
|
56 |
'cmd': lambda message: self.doCommand(message), |
|
dvander@117
|
57 |
'ln': lambda message: self.doSymlink(message), |
|
dvander@117
|
58 |
'cp': lambda message: self.doCopy(message), |
|
dvander@190
|
59 |
'rc': lambda message: self.doResource(message), |
|
dvander@94
|
60 |
} |
|
dvander@87
|
61 |
|
|
dvander@131
|
62 |
self.channel.send({ |
|
dvander@131
|
63 |
'id': 'ready', |
|
dvander@131
|
64 |
'finished': None |
|
dvander@131
|
65 |
}) |
|
dvander@89
|
66 |
|
|
dvander@93
|
67 |
def receiveTask(self, channel, message): |
|
dvander@94
|
68 |
task_id = message['task_id'] |
|
dvander@94
|
69 |
task_type = message['task_type'] |
|
dvander@94
|
70 |
task_folder = message['task_folder'] |
|
dvander@94
|
71 |
if not task_folder: |
|
dvander@94
|
72 |
task_folder = '.' |
|
dvander@94
|
73 |
|
|
dvander@94
|
74 |
# Remove all outputs. |
|
dvander@94
|
75 |
for output in message['task_outputs']: |
|
dvander@94
|
76 |
try: |
|
dvander@94
|
77 |
os.unlink(output) |
|
dvander@94
|
78 |
except OSError as exn: |
|
dvander@94
|
79 |
if exn.errno != errno.ENOENT: |
|
danderson@132
|
80 |
response = { |
|
danderson@132
|
81 |
'ok': False, |
|
danderson@132
|
82 |
'cmdline': 'rm {0}'.format(output), |
|
danderson@132
|
83 |
'stdout': '', |
|
danderson@132
|
84 |
'stderr': '{0}'.format(exn) |
|
danderson@132
|
85 |
} |
|
danderson@132
|
86 |
return self.issueResponse(message, response) |
|
dvander@94
|
87 |
|
|
dvander@124
|
88 |
if not message['task_folder']: |
|
dvander@124
|
89 |
message['task_folder'] = '.' |
|
dvander@124
|
90 |
|
|
dvander@94
|
91 |
# Do the task. |
|
dvander@94
|
92 |
response = self.taskMap[task_type](message) |
|
danderson@132
|
93 |
return self.issueResponse(message, response) |
|
dvander@94
|
94 |
|
|
danderson@132
|
95 |
def issueResponse(self, message, response): |
|
dvander@94
|
96 |
# Compute new timestamps for all command outputs. |
|
dvander@94
|
97 |
new_timestamps = [] |
|
dvander@94
|
98 |
if response['ok']: |
|
dvander@94
|
99 |
for output in message['task_outputs']: |
|
dvander@185
|
100 |
try: |
|
dvander@185
|
101 |
new_timestamps.append((output, os.path.getmtime(output))) |
|
dvander@185
|
102 |
except: |
|
dvander@185
|
103 |
response['ok'] = False |
|
dvander@185
|
104 |
response['stderr'] += 'Expected output file, but not found: {0}'.format(output) |
|
dvander@185
|
105 |
break |
|
dvander@94
|
106 |
|
|
dvander@94
|
107 |
# Send a message back to the master process to update the DAG and spew |
|
dvander@94
|
108 |
# stdout/stderr if needed. |
|
dvander@94
|
109 |
response['id'] = 'results' |
|
dvander@94
|
110 |
response['pid'] = self.pid |
|
danderson@132
|
111 |
response['task_id'] = message['task_id'] |
|
dvander@94
|
112 |
response['updates'] = new_timestamps |
|
dvander@142
|
113 |
self.channel.send(response) |
|
dvander@94
|
114 |
|
|
dvander@94
|
115 |
def doCommand(self, message): |
|
dvander@94
|
116 |
task_folder = message['task_folder'] |
|
dvander@130
|
117 |
argv = message['task_data'] |
|
dvander@94
|
118 |
with util.FolderChanger(task_folder): |
|
danderson@133
|
119 |
try: |
|
danderson@133
|
120 |
p, stdout, stderr = util.Execute(argv) |
|
danderson@133
|
121 |
status = p.returncode == 0 |
|
danderson@133
|
122 |
except Exception as exn: |
|
danderson@133
|
123 |
status = False |
|
danderson@133
|
124 |
stdout = '' |
|
danderson@133
|
125 |
stderr = '{0}'.format(exn) |
|
dvander@94
|
126 |
|
|
dvander@94
|
127 |
reply = { |
|
danderson@133
|
128 |
'ok': status, |
|
dvander@130
|
129 |
'cmdline': ' '.join([arg for arg in argv]), |
|
dvander@94
|
130 |
'stdout': stdout, |
|
dvander@115
|
131 |
'stderr': stderr, |
|
dvander@115
|
132 |
} |
|
dvander@115
|
133 |
return reply |
|
dvander@115
|
134 |
|
|
dvander@115
|
135 |
def doSymlink(self, message): |
|
dvander@115
|
136 |
task_folder = message['task_folder'] |
|
dvander@115
|
137 |
source_path, output_path = message['task_data'] |
|
dvander@115
|
138 |
|
|
dvander@115
|
139 |
with util.FolderChanger(task_folder): |
|
dvander@168
|
140 |
rcode, stdout, stderr = util.symlink(source_path, output_path) |
|
dvander@115
|
141 |
|
|
dvander@115
|
142 |
reply = { |
|
dvander@128
|
143 |
'ok': rcode == 0, |
|
dvander@130
|
144 |
'cmdline': 'ln -s {0} {1}'.format(source_path, output_path), |
|
dvander@130
|
145 |
'stdout': stdout, |
|
dvander@128
|
146 |
'stderr': stderr, |
|
dvander@117
|
147 |
} |
|
dvander@117
|
148 |
return reply |
|
dvander@117
|
149 |
|
|
dvander@117
|
150 |
def doCopy(self, message): |
|
dvander@117
|
151 |
task_folder = message['task_folder'] |
|
dvander@117
|
152 |
source_path, output_path = message['task_data'] |
|
dvander@117
|
153 |
|
|
dvander@117
|
154 |
with util.FolderChanger(task_folder): |
|
dvander@139
|
155 |
if os.path.exists(source_path): |
|
dvander@139
|
156 |
shutil.copy(source_path, output_path) |
|
dvander@139
|
157 |
ok = True |
|
dvander@139
|
158 |
stderr = '' |
|
dvander@139
|
159 |
else: |
|
dvander@139
|
160 |
ok = False |
|
dvander@139
|
161 |
stderr = 'File not found: {0}'.format(source_path) |
|
dvander@117
|
162 |
|
|
dvander@117
|
163 |
reply = { |
|
dvander@139
|
164 |
'ok': ok, |
|
dvander@131
|
165 |
'cmdline': 'cp "{0}" "{1}"'.format(source_path, os.path.join(task_folder, output_path)), |
|
dvander@130
|
166 |
'stdout': '', |
|
dvander@139
|
167 |
'stderr': stderr, |
|
dvander@94
|
168 |
} |
|
dvander@94
|
169 |
return reply |
|
dvander@94
|
170 |
|
|
dvander@190
|
171 |
# Adjusts any dependencies relative to the current folder, to be relative to |
|
dvander@190
|
172 |
# the output folder instead. |
|
dvander@190
|
173 |
def rewriteDeps(self, deps): |
|
dvander@190
|
174 |
paths = [] |
|
dvander@190
|
175 |
for inc_path in deps: |
|
dvander@190
|
176 |
if not os.path.isabs(inc_path): |
|
dvander@190
|
177 |
inc_path = os.path.abspath(inc_path) |
|
dvander@190
|
178 |
|
|
dvander@190
|
179 |
# Detect whether the include is within the build folder or not. |
|
dvander@190
|
180 |
build_path = self.buildPath |
|
dvander@190
|
181 |
if build_path[-1] != '/': |
|
dvander@190
|
182 |
build_path += '/' |
|
dvander@190
|
183 |
prefix = os.path.commonprefix([build_path, inc_path]) |
|
dvander@190
|
184 |
if prefix == build_path: |
|
dvander@190
|
185 |
# The include is not a system include, i.e. it was generated, so |
|
dvander@190
|
186 |
# rewrite the path to be relative to the build folder. |
|
dvander@190
|
187 |
inc_path = os.path.relpath(inc_path, self.buildPath) |
|
dvander@190
|
188 |
|
|
dvander@190
|
189 |
paths.append(inc_path) |
|
dvander@190
|
190 |
return paths |
|
dvander@190
|
191 |
|
|
dvander@94
|
192 |
def doCompile(self, message): |
|
dvander@94
|
193 |
task_folder = message['task_folder'] |
|
dvander@94
|
194 |
task_data = message['task_data'] |
|
dvander@94
|
195 |
cc_type = task_data['type'] |
|
dvander@94
|
196 |
argv = task_data['argv'] |
|
dvander@94
|
197 |
|
|
dvander@94
|
198 |
with util.FolderChanger(task_folder): |
|
dvander@94
|
199 |
p, out, err = util.Execute(argv) |
|
dvander@94
|
200 |
if cc_type == 'gcc': |
|
dvander@94
|
201 |
err, deps = util.ParseGCCDeps(err) |
|
dvander@128
|
202 |
elif cc_type == 'msvc': |
|
dvander@128
|
203 |
out, deps = util.ParseMSVCDeps(out) |
|
dvander@183
|
204 |
elif cc_type == 'sun': |
|
dvander@183
|
205 |
err, deps = util.ParseSunDeps(err) |
|
dvander@94
|
206 |
else: |
|
dvander@94
|
207 |
raise Exception('unknown compiler type') |
|
dvander@100
|
208 |
|
|
dvander@190
|
209 |
paths = self.rewriteDeps(deps) |
|
dvander@94
|
210 |
|
|
dvander@94
|
211 |
reply = { |
|
dvander@94
|
212 |
'ok': p.returncode == 0, |
|
dvander@130
|
213 |
'cmdline': ' '.join([arg for arg in argv]), |
|
dvander@94
|
214 |
'stdout': out, |
|
dvander@94
|
215 |
'stderr': err, |
|
dvander@94
|
216 |
'deps': paths, |
|
dvander@94
|
217 |
} |
|
dvander@94
|
218 |
return reply |
|
dvander@87
|
219 |
|
|
dvander@190
|
220 |
def doResource(self, message): |
|
dvander@190
|
221 |
task_folder = message['task_folder'] |
|
dvander@190
|
222 |
task_data = message['task_data'] |
|
dvander@190
|
223 |
cl_argv = task_data['cl_argv'] |
|
dvander@190
|
224 |
rc_argv = task_data['rc_argv'] |
|
dvander@190
|
225 |
|
|
dvander@190
|
226 |
with util.FolderChanger(task_folder): |
|
dvander@190
|
227 |
# Includes go to stderr when we preprocess to stdout. |
|
dvander@204
|
228 |
p, out, err = util.Execute(cl_argv) |
|
dvander@204
|
229 |
out, deps = util.ParseMSVCDeps(err) |
|
dvander@190
|
230 |
paths = self.rewriteDeps(deps) |
|
dvander@190
|
231 |
|
|
dvander@190
|
232 |
if p.returncode == 0: |
|
dvander@190
|
233 |
p, out, err = util.Execute(rc_argv) |
|
dvander@190
|
234 |
|
|
dvander@190
|
235 |
reply = { |
|
dvander@190
|
236 |
'ok': p.returncode == 0, |
|
dvander@199
|
237 |
'cmdline': (' '.join([arg for arg in cl_argv]) + ' && ' + ' '.join([arg for arg in rc_argv])), |
|
dvander@190
|
238 |
'stdout': out, |
|
dvander@190
|
239 |
'stderr': err, |
|
dvander@190
|
240 |
'deps': paths, |
|
dvander@190
|
241 |
} |
|
dvander@190
|
242 |
return reply |
|
dvander@190
|
243 |
|
|
dvander@190
|
244 |
|
|
dvander@92
|
245 |
# The WorkerParent is in the same process as the TaskMasterChild. |
|
dvander@94
|
246 |
class WorkerParent(ParentProcessListener): |
|
dvander@142
|
247 |
def __init__(self, taskMaster): |
|
dvander@92
|
248 |
super(WorkerParent, self).__init__('Worker') |
|
dvander@87
|
249 |
self.taskMaster = taskMaster |
|
dvander@93
|
250 |
self.messageMap = { |
|
dvander@94
|
251 |
'ready': lambda child, message: self.receiveReady(child, message), |
|
dvander@142
|
252 |
'results': lambda child, message: self.receiveResults(child, message), |
|
dvander@93
|
253 |
} |
|
dvander@87
|
254 |
|
|
dvander@142
|
255 |
def receiveReady(self, child, message): |
|
dvander@142
|
256 |
self.taskMaster.onWorkerReady(child, message) |
|
dvander@87
|
257 |
|
|
dvander@142
|
258 |
def receiveResults(self, child, message): |
|
dvander@142
|
259 |
self.taskMaster.onWorkerResults(child, message) |
|
dvander@92
|
260 |
|
|
dvander@88
|
261 |
def receiveError(self, child, error): |
|
dvander@93
|
262 |
self.taskMaster.onWorkerDied(child, error) |
|
dvander@88
|
263 |
|
|
dvander@92
|
264 |
# The TaskMasterChild is in the same process as the WorkerParent. |
|
dvander@94
|
265 |
class TaskMasterChild(ChildProcessListener): |
|
dvander@142
|
266 |
def __init__(self, pump, channel, task_graph, buildPath, num_processes): |
|
dvander@131
|
267 |
super(TaskMasterChild, self).__init__(pump, channel) |
|
dvander@93
|
268 |
self.task_graph = task_graph |
|
dvander@93
|
269 |
self.outstanding = {} |
|
dvander@95
|
270 |
self.idle = set() |
|
dvander@94
|
271 |
self.build_failed = False |
|
dvander@100
|
272 |
self.build_completed = False |
|
dvander@173
|
273 |
self.messageMap = { |
|
dvander@173
|
274 |
'stop': lambda channel, message: self.receiveStop(channel, message) |
|
dvander@173
|
275 |
} |
|
dvander@93
|
276 |
|
|
dvander@89
|
277 |
self.procman = ProcessManager(pump) |
|
dvander@142
|
278 |
for i in range(num_processes): |
|
dvander@94
|
279 |
self.procman.spawn( |
|
dvander@142
|
280 |
WorkerParent(self), |
|
dvander@94
|
281 |
WorkerChild, |
|
dvander@142
|
282 |
args=(buildPath,) |
|
dvander@94
|
283 |
) |
|
dvander@94
|
284 |
|
|
dvander@131
|
285 |
self.channel.send({ |
|
dvander@131
|
286 |
'id': 'spawned', |
|
dvander@142
|
287 |
'pid': os.getpid(), |
|
dvander@142
|
288 |
'type': 'taskmaster' |
|
dvander@131
|
289 |
}) |
|
dvander@131
|
290 |
|
|
dvander@173
|
291 |
def receiveStop(self, channel, message): |
|
dvander@173
|
292 |
if not message['ok']: |
|
dvander@174
|
293 |
self.terminateBuild() |
|
dvander@173
|
294 |
self.close_idle() |
|
dvander@173
|
295 |
|
|
dvander@174
|
296 |
def terminateBuild(self): |
|
dvander@173
|
297 |
if self.build_failed: |
|
dvander@173
|
298 |
return |
|
dvander@173
|
299 |
|
|
dvander@173
|
300 |
self.build_failed = True |
|
dvander@173
|
301 |
self.close_idle() |
|
dvander@94
|
302 |
|
|
dvander@94
|
303 |
def receiveClose(self, channel): |
|
dvander@94
|
304 |
self.procman.shutdown() |
|
dvander@173
|
305 |
self.pump.cancel() |
|
dvander@87
|
306 |
|
|
dvander@142
|
307 |
def onWorkerResults(self, child, message): |
|
dvander@142
|
308 |
# Forward the results to the master process. |
|
dvander@142
|
309 |
self.channel.send(message) |
|
dvander@142
|
310 |
|
|
dvander@94
|
311 |
if not message['ok']: |
|
dvander@94
|
312 |
self.channel.send({ |
|
dvander@94
|
313 |
'id': 'completed', |
|
dvander@94
|
314 |
'status': 'failed', |
|
dvander@94
|
315 |
}) |
|
dvander@94
|
316 |
self.procman.close(child) |
|
dvander@174
|
317 |
self.terminateBuild() |
|
dvander@94
|
318 |
return |
|
dvander@94
|
319 |
|
|
dvander@94
|
320 |
task_id = message['task_id'] |
|
dvander@94
|
321 |
task, child = self.outstanding[task_id] |
|
dvander@94
|
322 |
del self.outstanding[task_id] |
|
dvander@94
|
323 |
|
|
dvander@94
|
324 |
# Enqueue any tasks that can be run if this was their last outstanding |
|
dvander@94
|
325 |
# dependency. |
|
dvander@94
|
326 |
for outgoing in task.outgoing: |
|
dvander@94
|
327 |
outgoing.incoming.remove(task) |
|
dvander@94
|
328 |
if len(outgoing.incoming) == 0: |
|
dvander@94
|
329 |
self.task_graph.append(outgoing) |
|
dvander@94
|
330 |
|
|
dvander@142
|
331 |
self.onWorkerReady(child, None) |
|
dvander@89
|
332 |
|
|
dvander@95
|
333 |
# If more stuff was queued, and we have idle processes, use them. |
|
dvander@95
|
334 |
while len(self.task_graph) and len(self.idle): |
|
dvander@95
|
335 |
child = self.idle.pop() |
|
dvander@142
|
336 |
if not self.onWorkerReady(child, None): |
|
dvander@95
|
337 |
break |
|
dvander@95
|
338 |
|
|
dvander@97
|
339 |
def close_idle(self): |
|
dvander@97
|
340 |
for child in self.idle: |
|
dvander@97
|
341 |
self.procman.close(child) |
|
dvander@97
|
342 |
self.idle = set() |
|
dvander@97
|
343 |
|
|
dvander@142
|
344 |
def onWorkerReady(self, child, message): |
|
dvander@142
|
345 |
if message and not message['finished']: |
|
dvander@142
|
346 |
self.channel.send({ |
|
dvander@142
|
347 |
'id': 'spawned', |
|
dvander@142
|
348 |
'pid': child.pid, |
|
dvander@142
|
349 |
'type': 'worker' |
|
dvander@142
|
350 |
}) |
|
dvander@142
|
351 |
|
|
dvander@173
|
352 |
# If the build failed, ignore the message, and shutdown the process. |
|
dvander@100
|
353 |
if self.build_failed: |
|
dvander@173
|
354 |
self.procman.close(child) |
|
dvander@173
|
355 |
self.maybe_request_shutdown(child) |
|
dvander@173
|
356 |
return |
|
dvander@100
|
357 |
|
|
dvander@93
|
358 |
if not len(self.task_graph): |
|
dvander@93
|
359 |
if len(self.outstanding): |
|
dvander@93
|
360 |
# There are still tasks left to complete, but they're waiting on |
|
dvander@93
|
361 |
# others to finish. Mark this process as ready and just ignore the |
|
dvander@93
|
362 |
# status change for now. |
|
dvander@95
|
363 |
self.idle.add(child) |
|
dvander@93
|
364 |
else: |
|
dvander@93
|
365 |
# There are no tasks remaining, the worker is not needed. |
|
dvander@100
|
366 |
self.build_completed = True |
|
dvander@93
|
367 |
self.procman.close(child) |
|
dvander@97
|
368 |
self.close_idle() |
|
dvander@97
|
369 |
self.channel.send({ |
|
dvander@97
|
370 |
'id': 'completed', |
|
dvander@97
|
371 |
'status': 'ok' |
|
dvander@97
|
372 |
}) |
|
dvander@95
|
373 |
return False |
|
dvander@93
|
374 |
|
|
dvander@93
|
375 |
# Send a task to the worker. |
|
dvander@93
|
376 |
task = self.task_graph.pop() |
|
dvander@93
|
377 |
|
|
dvander@93
|
378 |
message = { |
|
dvander@93
|
379 |
'id': 'task', |
|
dvander@93
|
380 |
'task_id': task.id, |
|
dvander@93
|
381 |
'task_type': task.type, |
|
dvander@93
|
382 |
'task_data': task.data, |
|
dvander@94
|
383 |
'task_folder': task.folder, |
|
dvander@93
|
384 |
'task_outputs': task.outputs |
|
dvander@93
|
385 |
} |
|
dvander@93
|
386 |
child.send(message) |
|
dvander@93
|
387 |
self.outstanding[task.id] = (task, child) |
|
dvander@95
|
388 |
return True |
|
dvander@93
|
389 |
|
|
dvander@94
|
390 |
def onWorkerCrashed(self, child, task): |
|
dvander@94
|
391 |
self.channel.send({ |
|
dvander@94
|
392 |
'id': 'completed', |
|
dvander@94
|
393 |
'status': 'crashed', |
|
dvander@94
|
394 |
'task_id': task.id, |
|
dvander@94
|
395 |
}) |
|
dvander@174
|
396 |
self.terminateBuild() |
|
dvander@94
|
397 |
|
|
dvander@93
|
398 |
def onWorkerDied(self, child, error): |
|
dvander@93
|
399 |
if error != Error.NormalShutdown: |
|
dvander@93
|
400 |
for task_id in self.outstanding.keys(): |
|
dvander@94
|
401 |
task, task_child = self.outstanding[task_id] |
|
dvander@94
|
402 |
if task_child == child: |
|
dvander@94
|
403 |
# A worker failed, but crashed, so we have to tell the main process. |
|
dvander@94
|
404 |
self.onWorkerCrashed(child, task) |
|
dvander@94
|
405 |
break |
|
dvander@93
|
406 |
|
|
dvander@95
|
407 |
self.idle.discard(child) |
|
dvander@173
|
408 |
self.maybe_request_shutdown(child) |
|
dvander@94
|
409 |
|
|
dvander@173
|
410 |
def maybe_request_shutdown(self, child): |
|
dvander@94
|
411 |
for other_child in self.procman.children: |
|
dvander@94
|
412 |
if other_child == child: |
|
dvander@94
|
413 |
continue |
|
dvander@94
|
414 |
if other_child.is_alive(): |
|
dvander@94
|
415 |
return |
|
dvander@94
|
416 |
|
|
dvander@173
|
417 |
# If we got here, no other child processes are live, so we can ask for |
|
dvander@173
|
418 |
# safe shutdown. This is needed to make sure all our messages arrive, |
|
dvander@173
|
419 |
# since closing one end of the pipe destroys any leftover data. |
|
dvander@173
|
420 |
self.channel.send({ |
|
dvander@173
|
421 |
'id': 'done' |
|
dvander@173
|
422 |
}) |
|
dvander@89
|
423 |
|
|
dvander@94
|
424 |
class TaskMasterParent(ParentProcessListener): |
|
dvander@98
|
425 |
def __init__(self, cx, builder, task_graph, max_parallel): |
|
dvander@92
|
426 |
super(TaskMasterParent, self).__init__('TaskMaster') |
|
dvander@86
|
427 |
self.cx = cx |
|
dvander@94
|
428 |
self.builder = builder |
|
dvander@94
|
429 |
self.build_failed = False |
|
dvander@94
|
430 |
self.messageMap = { |
|
dvander@131
|
431 |
'completed': lambda child, message: self.receiveCompleted(child, message), |
|
dvander@131
|
432 |
'spawned': lambda child, message: self.receiveSpawned(message), |
|
dvander@173
|
433 |
'results': lambda child, message: self.processResults(message), |
|
dvander@173
|
434 |
'done': lambda child, message: self.receiveDone(message), |
|
dvander@94
|
435 |
} |
|
dvander@87
|
436 |
|
|
dvander@87
|
437 |
# Figure out how many tasks to create. |
|
dvander@87
|
438 |
if cx.options.jobs == 0: |
|
dvander@87
|
439 |
# Using 1 process will be strictly worse than an in-process build, |
|
dvander@87
|
440 |
# since we incur the additional overhead of message passing. Instead, |
|
dvander@87
|
441 |
# we use two processes as the minimal number. If that turns out to be |
|
dvander@87
|
442 |
# bad we can create an in-process TaskMaster later. |
|
dvander@91
|
443 |
if mp.cpu_count() == 1: |
|
dvander@91
|
444 |
num_processes = 2 |
|
dvander@91
|
445 |
else: |
|
dvander@152
|
446 |
num_processes = int(mp.cpu_count() * 1.25) |
|
dvander@99
|
447 |
else: |
|
dvander@99
|
448 |
num_processes = cx.options.jobs |
|
dvander@87
|
449 |
|
|
dvander@87
|
450 |
# Don't create more processes than we'll need. |
|
dvander@98
|
451 |
if num_processes > max_parallel: |
|
dvander@98
|
452 |
num_processes = max_parallel |
|
dvander@94
|
453 |
|
|
dvander@87
|
454 |
# Spawn the task master. |
|
dvander@173
|
455 |
self.taskMaster = cx.procman.spawn( |
|
dvander@94
|
456 |
self, |
|
dvander@94
|
457 |
TaskMasterChild, |
|
dvander@142
|
458 |
args=(task_graph, cx.buildPath, num_processes) |
|
dvander@94
|
459 |
) |
|
dvander@87
|
460 |
|
|
dvander@173
|
461 |
def receiveDone(self, message): |
|
dvander@173
|
462 |
self.cx.procman.shutdown() |
|
dvander@173
|
463 |
|
|
dvander@94
|
464 |
def processResults(self, message): |
|
dvander@130
|
465 |
if message['ok']: |
|
dvander@130
|
466 |
color = util.ConsoleGreen |
|
dvander@130
|
467 |
else: |
|
dvander@130
|
468 |
color = util.ConsoleRed |
|
dvander@130
|
469 |
util.con_out( |
|
dvander@130
|
470 |
util.ConsoleBlue, |
|
dvander@130
|
471 |
'[{0}]'.format(message['pid']), |
|
dvander@130
|
472 |
util.ConsoleNormal, |
|
dvander@130
|
473 |
' ', |
|
dvander@130
|
474 |
color, |
|
dvander@130
|
475 |
message['cmdline'], |
|
dvander@130
|
476 |
util.ConsoleNormal |
|
dvander@130
|
477 |
) |
|
dvander@94
|
478 |
if len(message['stdout']): |
|
dvander@130
|
479 |
sys.stdout.write(message['stdout']) |
|
dvander@128
|
480 |
if message['stdout'][-1] != '\n': |
|
dvander@128
|
481 |
sys.stdout.write('\n') |
|
dvander@94
|
482 |
if len(message['stderr']): |
|
dvander@94
|
483 |
sys.stderr.write(message['stderr']) |
|
dvander@128
|
484 |
if message['stderr'][-1] != '\n': |
|
dvander@128
|
485 |
sys.stderr.write('\n') |
|
dvander@94
|
486 |
|
|
dvander@94
|
487 |
if not message['ok']: |
|
dvander@173
|
488 |
self.terminateBuild() |
|
dvander@94
|
489 |
return |
|
dvander@94
|
490 |
|
|
dvander@94
|
491 |
task_id = message['task_id'] |
|
dvander@94
|
492 |
updates = message['updates'] |
|
dvander@141
|
493 |
if not self.builder.updateGraph(task_id, updates, message): |
|
dvander@130
|
494 |
util.con_out( |
|
dvander@130
|
495 |
util.ConsoleRed, |
|
dvander@130
|
496 |
'Failed to update node!', |
|
dvander@130
|
497 |
util.ConsoleNormal |
|
dvander@130
|
498 |
) |
|
dvander@173
|
499 |
self.terminateBuild() |
|
dvander@94
|
500 |
|
|
dvander@173
|
501 |
def terminateBuild(self): |
|
dvander@174
|
502 |
if self.build_failed: |
|
dvander@174
|
503 |
return |
|
dvander@174
|
504 |
self.build_failed = True |
|
dvander@174
|
505 |
self.taskMaster.send({ |
|
dvander@174
|
506 |
'id': 'stop', |
|
dvander@174
|
507 |
'ok': False |
|
dvander@174
|
508 |
}) |
|
dvander@94
|
509 |
|
|
dvander@131
|
510 |
def receiveSpawned(self, message): |
|
dvander@131
|
511 |
util.con_out( |
|
dvander@131
|
512 |
util.ConsoleHeader, |
|
dvander@142
|
513 |
'Spawned {0} (pid: {1})'.format(message['type'], message['pid']), |
|
dvander@131
|
514 |
util.ConsoleNormal |
|
dvander@131
|
515 |
) |
|
dvander@131
|
516 |
|
|
dvander@88
|
517 |
def receiveError(self, child, error): |
|
dvander@94
|
518 |
if error != Error.NormalShutdown: |
|
dvander@128
|
519 |
sys.stderr.write('Received unexpected error from child process {0}: {1}\n'.format(child.pid, error)) |
|
dvander@173
|
520 |
self.terminateBuild() |
|
dvander@94
|
521 |
|
|
dvander@94
|
522 |
def receiveCompleted(self, child, message): |
|
dvander@94
|
523 |
if message['status'] == 'crashed': |
|
dvander@142
|
524 |
task = self.builder.commands[message['task_id']] |
|
dvander@94
|
525 |
sys.stderr.write('Crashed trying to perform update:\n') |
|
dvander@94
|
526 |
sys.stderr.write(' : {0}\n'.format(task.entry.format())) |
|
dvander@173
|
527 |
self.terminateBuild() |
|
dvander@94
|
528 |
elif message['status'] == 'failed': |
|
dvander@173
|
529 |
self.terminateBuild() |
|
dvander@94
|
530 |
else: |
|
dvander@174
|
531 |
self.taskMaster.send({ |
|
dvander@174
|
532 |
'id': 'stop', |
|
dvander@174
|
533 |
'ok': True |
|
dvander@174
|
534 |
}) |
|
dvander@88
|
535 |
|
|
dvander@87
|
536 |
def run(self): |
|
dvander@87
|
537 |
self.cx.messagePump.pump() |
|
dvander@94
|
538 |
return not self.build_failed |