Take two at console decoding.
1 # vim: set ts=8 sts=2 sw=2 tw=99 et:
6 import multiprocessing as mp
7 from ambuild2 import util
8 from ambuild2 import nodetypes
9 from ambuild2.ipc import ParentProcessListener, ChildProcessListener
10 from ambuild2.ipc import ProcessManager, MessageListener, Error
11 from ambuild2.ipc import Channel
14 def __init__(self, id, entry, outputs):
16 self.type = entry.type
17 self.data = entry.blob
19 self.folder = entry.folder.path
22 self.outputs = outputs
26 def addOutgoing(self, task):
27 self.outgoing.append(task)
28 task.incoming.add(self)
31 def folder_name(self):
38 if self.type == nodetypes.Cxx:
39 return '[' + self.data['type'] + ']' + ' -> ' + (' '.join([arg for arg in self.data['argv']]))
40 if self.type == nodetypes.Symlink:
41 return 'ln -s "{0}" "{1}"'.format(self.data[0], os.path.join(self.folder_name, self.data[1]))
42 if self.type == nodetypes.Copy:
43 return 'cp "{0}" "{1}"'.format(self.data[0], os.path.join(self.folder_name, self.data[1]))
44 return (' '.join([arg for arg in self.data]))
46 class WorkerChild(ChildProcessListener):
47 def __init__(self, pump, channel, buildPath):
48 super(WorkerChild, self).__init__(pump, channel)
49 self.buildPath = buildPath
50 self.pid = os.getpid()
52 'task': lambda channel, message: self.receiveTask(channel, message)
55 'cxx': lambda message: self.doCompile(message),
56 'cmd': lambda message: self.doCommand(message),
57 'ln': lambda message: self.doSymlink(message),
58 'cp': lambda message: self.doCopy(message),
59 'rc': lambda message: self.doResource(message),
67 def receiveTask(self, channel, message):
68 task_id = message['task_id']
69 task_type = message['task_type']
70 task_folder = message['task_folder']
75 for output in message['task_outputs']:
78 except OSError as exn:
79 if exn.errno != errno.ENOENT:
82 'cmdline': 'rm {0}'.format(output),
84 'stderr': '{0}'.format(exn)
86 return self.issueResponse(message, response)
88 if not message['task_folder']:
89 message['task_folder'] = '.'
92 response = self.taskMap[task_type](message)
93 return self.issueResponse(message, response)
95 def issueResponse(self, message, response):
96 # Compute new timestamps for all command outputs.
99 for output in message['task_outputs']:
101 new_timestamps.append((output, os.path.getmtime(output)))
103 response['ok'] = False
104 response['stderr'] += 'Expected output file, but not found: {0}'.format(output)
107 # Send a message back to the master process to update the DAG and spew
108 # stdout/stderr if needed.
109 response['id'] = 'results'
110 response['pid'] = self.pid
111 response['task_id'] = message['task_id']
112 response['updates'] = new_timestamps
113 self.channel.send(response)
115 def doCommand(self, message):
116 task_folder = message['task_folder']
117 argv = message['task_data']
118 with util.FolderChanger(task_folder):
120 p, stdout, stderr = util.Execute(argv)
121 status = p.returncode == 0
122 except Exception as exn:
125 stderr = '{0}'.format(exn)
129 'cmdline': ' '.join([arg for arg in argv]),
135 def doSymlink(self, message):
136 task_folder = message['task_folder']
137 source_path, output_path = message['task_data']
139 with util.FolderChanger(task_folder):
140 rcode, stdout, stderr = util.symlink(source_path, output_path)
144 'cmdline': 'ln -s {0} {1}'.format(source_path, output_path),
150 def doCopy(self, message):
151 task_folder = message['task_folder']
152 source_path, output_path = message['task_data']
154 with util.FolderChanger(task_folder):
155 if os.path.exists(source_path):
156 shutil.copy(source_path, output_path)
161 stderr = 'File not found: {0}'.format(source_path)
165 'cmdline': 'cp "{0}" "{1}"'.format(source_path, os.path.join(task_folder, output_path)),
171 # Adjusts any dependencies relative to the current folder, to be relative to
172 # the output folder instead.
173 def rewriteDeps(self, deps):
175 for inc_path in deps:
176 if not os.path.isabs(inc_path):
177 inc_path = os.path.abspath(inc_path)
179 # Detect whether the include is within the build folder or not.
180 build_path = self.buildPath
181 if build_path[-1] != os.path.sep:
182 build_path += os.path.sep
183 prefix = os.path.commonprefix([build_path, inc_path])
184 if prefix == build_path:
185 # The include is not a system include, i.e. it was generated, so
186 # rewrite the path to be relative to the build folder.
187 inc_path = os.path.relpath(inc_path, self.buildPath)
189 paths.append(inc_path)
192 def doCompile(self, message):
193 task_folder = message['task_folder']
194 task_data = message['task_data']
195 cc_type = task_data['type']
196 argv = task_data['argv']
198 with util.FolderChanger(task_folder):
199 p, out, err = util.Execute(argv)
201 err, deps = util.ParseGCCDeps(err)
202 elif cc_type == 'msvc':
203 out, deps = util.ParseMSVCDeps(out)
204 elif cc_type == 'sun':
205 err, deps = util.ParseSunDeps(err)
207 raise Exception('unknown compiler type')
209 paths = self.rewriteDeps(deps)
212 'ok': p.returncode == 0,
213 'cmdline': ' '.join([arg for arg in argv]),
220 def doResource(self, message):
221 task_folder = message['task_folder']
222 task_data = message['task_data']
223 cl_argv = task_data['cl_argv']
224 rc_argv = task_data['rc_argv']
226 with util.FolderChanger(task_folder):
227 # Includes go to stderr when we preprocess to stdout.
228 p, out, err = util.Execute(cl_argv)
229 out, deps = util.ParseMSVCDeps(err)
230 paths = self.rewriteDeps(deps)
232 if p.returncode == 0:
233 p, out, err = util.Execute(rc_argv)
236 'ok': p.returncode == 0,
237 'cmdline': (' '.join([arg for arg in cl_argv]) + ' && ' + ' '.join([arg for arg in rc_argv])),
245 # The WorkerParent is in the same process as the TaskMasterChild.
246 class WorkerParent(ParentProcessListener):
247 def __init__(self, taskMaster):
248 super(WorkerParent, self).__init__('Worker')
249 self.taskMaster = taskMaster
251 'ready': lambda child, message: self.receiveReady(child, message),
252 'results': lambda child, message: self.receiveResults(child, message),
255 def receiveReady(self, child, message):
256 self.taskMaster.onWorkerReady(child, message)
258 def receiveResults(self, child, message):
259 self.taskMaster.onWorkerResults(child, message)
261 def receiveError(self, child, error):
262 self.taskMaster.onWorkerDied(child, error)
264 # The TaskMasterChild is in the same process as the WorkerParent.
265 class TaskMasterChild(ChildProcessListener):
266 def __init__(self, pump, channel, task_graph, buildPath, num_processes):
267 super(TaskMasterChild, self).__init__(pump, channel)
268 self.task_graph = task_graph
269 self.outstanding = {}
271 self.build_failed = False
272 self.build_completed = False
274 'stop': lambda channel, message: self.receiveStop(channel, message)
277 self.procman = ProcessManager(pump)
278 for i in range(num_processes):
291 def receiveStop(self, channel, message):
292 if not message['ok']:
293 self.terminateBuild()
296 def terminateBuild(self):
297 if self.build_failed:
300 self.build_failed = True
303 def receiveClose(self, channel):
304 self.procman.shutdown()
307 def onWorkerResults(self, child, message):
308 # Forward the results to the master process.
309 self.channel.send(message)
311 if not message['ok']:
316 self.procman.close(child)
317 self.terminateBuild()
320 task_id = message['task_id']
321 task, child = self.outstanding[task_id]
322 del self.outstanding[task_id]
324 # Enqueue any tasks that can be run if this was their last outstanding
326 for outgoing in task.outgoing:
327 outgoing.incoming.remove(task)
328 if len(outgoing.incoming) == 0:
329 self.task_graph.append(outgoing)
331 self.onWorkerReady(child, None)
333 # If more stuff was queued, and we have idle processes, use them.
334 while len(self.task_graph) and len(self.idle):
335 child = self.idle.pop()
336 if not self.onWorkerReady(child, None):
339 def close_idle(self):
340 for child in self.idle:
341 self.procman.close(child)
344 def onWorkerReady(self, child, message):
345 if message and not message['finished']:
352 # If the build failed, ignore the message, and shutdown the process.
353 if self.build_failed:
354 self.procman.close(child)
355 self.maybe_request_shutdown(child)
358 if not len(self.task_graph):
359 if len(self.outstanding):
360 # There are still tasks left to complete, but they're waiting on
361 # others to finish. Mark this process as ready and just ignore the
362 # status change for now.
365 # There are no tasks remaining, the worker is not needed.
366 self.build_completed = True
367 self.procman.close(child)
375 # Send a task to the worker.
376 task = self.task_graph.pop()
381 'task_type': task.type,
382 'task_data': task.data,
383 'task_folder': task.folder,
384 'task_outputs': task.outputs
387 self.outstanding[task.id] = (task, child)
390 def onWorkerCrashed(self, child, task):
396 self.terminateBuild()
398 def onWorkerDied(self, child, error):
399 if error != Error.NormalShutdown:
400 for task_id in self.outstanding.keys():
401 task, task_child = self.outstanding[task_id]
402 if task_child == child:
403 # A worker failed, but crashed, so we have to tell the main process.
404 self.onWorkerCrashed(child, task)
407 self.idle.discard(child)
408 self.maybe_request_shutdown(child)
410 def maybe_request_shutdown(self, child):
411 for other_child in self.procman.children:
412 if other_child == child:
414 if other_child.is_alive():
417 # If we got here, no other child processes are live, so we can ask for
418 # safe shutdown. This is needed to make sure all our messages arrive,
419 # since closing one end of the pipe destroys any leftover data.
424 class TaskMasterParent(ParentProcessListener):
425 def __init__(self, cx, builder, task_graph, max_parallel):
426 super(TaskMasterParent, self).__init__('TaskMaster')
428 self.builder = builder
429 self.build_failed = False
431 'completed': lambda child, message: self.receiveCompleted(child, message),
432 'spawned': lambda child, message: self.receiveSpawned(message),
433 'results': lambda child, message: self.processResults(message),
434 'done': lambda child, message: self.receiveDone(message),
438 # Figure out how many tasks to create.
439 if cx.options.jobs == 0:
440 # Using 1 process will be strictly worse than an in-process build,
441 # since we incur the additional overhead of message passing. Instead,
442 # we use two processes as the minimal number. If that turns out to be
443 # bad we can create an in-process TaskMaster later.
444 if mp.cpu_count() == 1:
447 num_processes = int(mp.cpu_count() * 1.25)
449 num_processes = cx.options.jobs
451 # Don't create more processes than we'll need.
452 if num_processes > max_parallel:
453 num_processes = max_parallel
455 # Spawn the task master.
456 self.taskMaster = cx.procman.spawn(
459 args=(task_graph, cx.buildPath, num_processes)
462 def receiveDone(self, message):
463 self.cx.procman.shutdown()
465 def spewResult(self, message):
467 color = util.ConsoleGreen
469 color = util.ConsoleRed
472 '[{0}]'.format(message['pid']),
479 if len(message['stdout']):
480 util.WriteEncodedText(sys.stdout, message['stdout'])
481 if message['stdout'][-1] != '\n':
482 sys.stdout.write('\n')
483 if len(message['stderr']):
484 util.WriteEncodedText(sys.stderr, message['stderr'])
485 if message['stderr'][-1] != '\n':
486 sys.stderr.write('\n')
488 def processResults(self, message):
489 if not message['ok']:
490 self.errors_.append(message)
491 self.terminateBuild()
494 self.spewResult(message)
496 task_id = message['task_id']
497 updates = message['updates']
498 if not self.builder.updateGraph(task_id, updates, message):
501 'Failed to update node!',
504 self.terminateBuild()
506 def terminateBuild(self):
507 if self.build_failed:
509 self.build_failed = True
510 self.taskMaster.send({
515 def receiveSpawned(self, message):
518 'Spawned {0} (pid: {1})'.format(message['type'], message['pid']),
522 def receiveError(self, child, error):
523 if error != Error.NormalShutdown:
524 sys.stderr.write('Received unexpected error from child process {0}: {1}\n'.format(child.pid, error))
525 self.terminateBuild()
527 def receiveCompleted(self, child, message):
528 if message['status'] == 'crashed':
529 task = self.builder.commands[message['task_id']]
530 sys.stderr.write('Crashed trying to perform update:\n')
531 sys.stderr.write(' : {0}\n'.format(task.entry.format()))
532 self.terminateBuild()
533 elif message['status'] == 'failed':
534 self.terminateBuild()
536 self.taskMaster.send({
543 self.cx.messagePump.pump()
544 except KeyboardInterrupt:
545 self.terminateBuild()
546 for message in self.errors_:
547 self.spewResult(message)
548 return not self.build_failed