ambuild2/task.py
author David Anderson <dvander@alliedmods.net>
Sun Feb 02 14:00:01 2014 -0800 (2014-02-02)
changeset 235 fd0f5f1a7a10
parent 224 11f656dae71a
child 243 eaa3bec37766
permissions -rw-r--r--
Clean and vacuum unused source nodes after reconfiguring; also don't add nodes for configure files.
     1 # vim: set ts=8 sts=2 sw=2 tw=99 et:
     2 import errno
     3 import shutil
     4 import os, sys
     5 import traceback
     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
    12 
    13 class Task(object):
    14   def __init__(self, id, entry, outputs):
    15     self.id = id
    16     self.type = entry.type
    17     self.data = entry.blob
    18     if entry.folder:
    19       self.folder = entry.folder.path
    20     else:
    21       self.folder = None
    22     self.outputs = outputs
    23     self.outgoing = []
    24     self.incoming = set()
    25 
    26   def addOutgoing(self, task):
    27     self.outgoing.append(task)
    28     task.incoming.add(self)
    29 
    30   @property
    31   def folder_name(self):
    32     if not self.folder:
    33       return ''
    34     return self.folder
    35 
    36   def format(self):
    37     text = ''
    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]))
    45 
    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()
    51     self.messageMap = {
    52       'task': lambda channel, message: self.receiveTask(channel, message)
    53     }
    54     self.taskMap = {
    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),
    60     }
    61 
    62     self.channel.send({
    63       'id': 'ready',
    64       'finished': None
    65     })
    66 
    67   def receiveTask(self, channel, message):
    68     task_id = message['task_id']
    69     task_type = message['task_type']
    70     task_folder = message['task_folder']
    71     if not task_folder:
    72       task_folder = '.'
    73 
    74     # Remove all outputs.
    75     for output in message['task_outputs']:
    76       try:
    77         os.unlink(output)
    78       except OSError as exn:
    79         if exn.errno != errno.ENOENT:
    80           response = {
    81             'ok': False,
    82             'cmdline': 'rm {0}'.format(output),
    83             'stdout': '',
    84             'stderr': '{0}'.format(exn)
    85           }
    86           return self.issueResponse(message, response)
    87 
    88     if not message['task_folder']:
    89       message['task_folder'] = '.'
    90 
    91     # Do the task.
    92     response = self.taskMap[task_type](message)
    93     return self.issueResponse(message, response)
    94 
    95   def issueResponse(self, message, response):
    96     # Compute new timestamps for all command outputs.
    97     new_timestamps = []
    98     if response['ok']:
    99       for output in message['task_outputs']:
   100         try:
   101           new_timestamps.append((output, os.path.getmtime(output)))
   102         except:
   103           response['ok'] = False
   104           response['stderr'] += 'Expected output file, but not found: {0}'.format(output)
   105           break
   106 
   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)
   114 
   115   def doCommand(self, message):
   116     task_folder = message['task_folder']
   117     argv = message['task_data']
   118     with util.FolderChanger(task_folder):
   119       try:
   120         p, stdout, stderr = util.Execute(argv)
   121         status = p.returncode == 0
   122       except Exception as exn:
   123         status = False
   124         stdout = ''
   125         stderr = '{0}'.format(exn)
   126 
   127     reply = {
   128       'ok': status,
   129       'cmdline': ' '.join([arg for arg in argv]),
   130       'stdout': stdout,
   131       'stderr': stderr,
   132     }
   133     return reply
   134 
   135   def doSymlink(self, message):
   136     task_folder = message['task_folder']
   137     source_path, output_path = message['task_data']
   138 
   139     with util.FolderChanger(task_folder):
   140       rcode, stdout, stderr = util.symlink(source_path, output_path)
   141 
   142     reply = {
   143       'ok': rcode == 0,
   144       'cmdline': 'ln -s {0} {1}'.format(source_path, output_path),
   145       'stdout': stdout,
   146       'stderr': stderr,
   147     }
   148     return reply
   149 
   150   def doCopy(self, message):
   151     task_folder = message['task_folder']
   152     source_path, output_path = message['task_data']
   153 
   154     with util.FolderChanger(task_folder):
   155       if os.path.exists(source_path):
   156         shutil.copy(source_path, output_path)
   157         ok = True
   158         stderr = ''
   159       else:
   160         ok = False
   161         stderr = 'File not found: {0}'.format(source_path)
   162 
   163     reply = {
   164       'ok': ok,
   165       'cmdline': 'cp "{0}" "{1}"'.format(source_path, os.path.join(task_folder, output_path)),
   166       'stdout': '',
   167       'stderr': stderr,
   168     }
   169     return reply
   170 
   171   # Adjusts any dependencies relative to the current folder, to be relative to
   172   # the output folder instead.
   173   def rewriteDeps(self, deps):
   174     paths = []
   175     for inc_path in deps:
   176       if not os.path.isabs(inc_path):
   177         inc_path = os.path.abspath(inc_path)
   178 
   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)
   188 
   189       paths.append(inc_path)
   190     return paths
   191 
   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']
   197 
   198     with util.FolderChanger(task_folder):
   199       p, out, err = util.Execute(argv)
   200       if cc_type == 'gcc':
   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)
   206       else:
   207         raise Exception('unknown compiler type')
   208 
   209       paths = self.rewriteDeps(deps)
   210 
   211     reply = {
   212       'ok': p.returncode == 0,
   213       'cmdline': ' '.join([arg for arg in argv]),
   214       'stdout': out,
   215       'stderr': err,
   216       'deps': paths,
   217     }
   218     return reply
   219 
   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']
   225 
   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)
   231 
   232       if p.returncode == 0:
   233         p, out, err = util.Execute(rc_argv)
   234         
   235     reply = {
   236       'ok': p.returncode == 0,
   237       'cmdline': (' '.join([arg for arg in cl_argv]) + ' && ' + ' '.join([arg for arg in rc_argv])),
   238       'stdout': out,
   239       'stderr': err,
   240       'deps': paths,
   241     }
   242     return reply
   243 
   244 
   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
   250     self.messageMap = {
   251       'ready': lambda child, message: self.receiveReady(child, message),
   252       'results': lambda child, message: self.receiveResults(child, message),
   253     }
   254 
   255   def receiveReady(self, child, message):
   256     self.taskMaster.onWorkerReady(child, message)
   257 
   258   def receiveResults(self, child, message):
   259     self.taskMaster.onWorkerResults(child, message)
   260 
   261   def receiveError(self, child, error):
   262     self.taskMaster.onWorkerDied(child, error)
   263 
   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 = {}
   270     self.idle = set()
   271     self.build_failed = False
   272     self.build_completed = False
   273     self.messageMap = {
   274       'stop': lambda channel, message: self.receiveStop(channel, message)
   275     }
   276 
   277     self.procman = ProcessManager(pump)
   278     for i in range(num_processes):
   279       self.procman.spawn(
   280         WorkerParent(self),
   281         WorkerChild,
   282         args=(buildPath,)
   283       )
   284 
   285     self.channel.send({
   286       'id': 'spawned',
   287       'pid': os.getpid(),
   288       'type': 'taskmaster'
   289     })
   290 
   291   def receiveStop(self, channel, message):
   292     if not message['ok']:
   293       self.terminateBuild()
   294     self.close_idle()
   295 
   296   def terminateBuild(self):
   297     if self.build_failed:
   298       return
   299 
   300     self.build_failed = True
   301     self.close_idle()
   302 
   303   def receiveClose(self, channel):
   304     self.procman.shutdown()
   305     self.pump.cancel()
   306 
   307   def onWorkerResults(self, child, message):
   308     # Forward the results to the master process.
   309     self.channel.send(message)
   310 
   311     if not message['ok']:
   312       self.channel.send({
   313         'id': 'completed',
   314         'status': 'failed',
   315       })
   316       self.procman.close(child)
   317       self.terminateBuild()
   318       return
   319 
   320     task_id = message['task_id']
   321     task, child = self.outstanding[task_id]
   322     del self.outstanding[task_id]
   323 
   324     # Enqueue any tasks that can be run if this was their last outstanding
   325     # dependency.
   326     for outgoing in task.outgoing:
   327       outgoing.incoming.remove(task)
   328       if len(outgoing.incoming) == 0:
   329         self.task_graph.append(outgoing)
   330 
   331     self.onWorkerReady(child, None)
   332 
   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):
   337         break
   338 
   339   def close_idle(self):
   340     for child in self.idle:
   341       self.procman.close(child)
   342     self.idle = set()
   343 
   344   def onWorkerReady(self, child, message):
   345     if message and not message['finished']:
   346       self.channel.send({
   347         'id': 'spawned',
   348         'pid': child.pid,
   349         'type': 'worker'
   350       })
   351 
   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)
   356       return
   357 
   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.
   363         self.idle.add(child)
   364       else:
   365         # There are no tasks remaining, the worker is not needed.
   366         self.build_completed = True
   367         self.procman.close(child)
   368         self.close_idle()
   369         self.channel.send({
   370           'id': 'completed',
   371           'status': 'ok'
   372         })
   373       return False
   374 
   375     # Send a task to the worker.
   376     task = self.task_graph.pop()
   377 
   378     message = {
   379       'id': 'task',
   380       'task_id': task.id,
   381       'task_type': task.type,
   382       'task_data': task.data,
   383       'task_folder': task.folder,
   384       'task_outputs': task.outputs
   385     }
   386     child.send(message)
   387     self.outstanding[task.id] = (task, child)
   388     return True
   389 
   390   def onWorkerCrashed(self, child, task):
   391     self.channel.send({
   392       'id': 'completed',
   393       'status': 'crashed',
   394       'task_id': task.id,
   395     })
   396     self.terminateBuild()
   397 
   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)
   405           break
   406 
   407     self.idle.discard(child)
   408     self.maybe_request_shutdown(child)
   409 
   410   def maybe_request_shutdown(self, child):
   411     for other_child in self.procman.children:
   412       if other_child == child:
   413         continue
   414       if other_child.is_alive():
   415         return
   416 
   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.
   420     self.channel.send({
   421       'id': 'done'
   422     })
   423 
   424 class TaskMasterParent(ParentProcessListener):
   425   def __init__(self, cx, builder, task_graph, max_parallel):
   426     super(TaskMasterParent, self).__init__('TaskMaster')
   427     self.cx = cx
   428     self.builder = builder
   429     self.build_failed = False
   430     self.messageMap = {
   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),
   435     }
   436     self.errors_ = []
   437 
   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:
   445         num_processes = 2
   446       else:
   447         num_processes = int(mp.cpu_count() * 1.25)
   448     else:
   449       num_processes = cx.options.jobs
   450 
   451     # Don't create more processes than we'll need.
   452     if num_processes > max_parallel:
   453       num_processes = max_parallel
   454 
   455     # Spawn the task master.
   456     self.taskMaster = cx.procman.spawn(
   457       self,
   458       TaskMasterChild,
   459       args=(task_graph, cx.buildPath, num_processes)
   460     )
   461 
   462   def receiveDone(self, message):
   463     self.cx.procman.shutdown()
   464 
   465   def spewResult(self, message):
   466     if message['ok']:
   467       color = util.ConsoleGreen
   468     else:
   469       color = util.ConsoleRed
   470     util.con_out(
   471       util.ConsoleBlue,
   472       '[{0}]'.format(message['pid']),
   473       util.ConsoleNormal,
   474       ' ',
   475       color,
   476       message['cmdline'],
   477       util.ConsoleNormal
   478     )
   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')
   487 
   488   def processResults(self, message):
   489     if not message['ok']:
   490       self.errors_.append(message)
   491       self.terminateBuild()
   492       return
   493 
   494     self.spewResult(message)
   495 
   496     task_id = message['task_id']
   497     updates = message['updates']
   498     if not self.builder.updateGraph(task_id, updates, message):
   499       util.con_out(
   500         util.ConsoleRed,
   501         'Failed to update node!',
   502         util.ConsoleNormal
   503       )
   504       self.terminateBuild()
   505 
   506   def terminateBuild(self):
   507     if self.build_failed:
   508       return
   509     self.build_failed = True
   510     self.taskMaster.send({
   511       'id': 'stop',
   512       'ok': False
   513     })
   514 
   515   def receiveSpawned(self, message):
   516     util.con_out(
   517       util.ConsoleHeader,
   518       'Spawned {0} (pid: {1})'.format(message['type'], message['pid']),
   519       util.ConsoleNormal
   520     )
   521 
   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()
   526 
   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()
   535     else:
   536       self.taskMaster.send({
   537         'id': 'stop',
   538         'ok': True
   539       })
   540 
   541   def run(self):
   542     try:
   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