| Index: scons-2.0.1/engine/SCons/Job.py
 | 
| ===================================================================
 | 
| --- scons-2.0.1/engine/SCons/Job.py	(revision 0)
 | 
| +++ scons-2.0.1/engine/SCons/Job.py	(revision 0)
 | 
| @@ -0,0 +1,435 @@
 | 
| +"""SCons.Job
 | 
| +
 | 
| +This module defines the Serial and Parallel classes that execute tasks to
 | 
| +complete a build. The Jobs class provides a higher level interface to start,
 | 
| +stop, and wait on jobs.
 | 
| +
 | 
| +"""
 | 
| +
 | 
| +#
 | 
| +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 The SCons Foundation
 | 
| +#
 | 
| +# Permission is hereby granted, free of charge, to any person obtaining
 | 
| +# a copy of this software and associated documentation files (the
 | 
| +# "Software"), to deal in the Software without restriction, including
 | 
| +# without limitation the rights to use, copy, modify, merge, publish,
 | 
| +# distribute, sublicense, and/or sell copies of the Software, and to
 | 
| +# permit persons to whom the Software is furnished to do so, subject to
 | 
| +# the following conditions:
 | 
| +#
 | 
| +# The above copyright notice and this permission notice shall be included
 | 
| +# in all copies or substantial portions of the Software.
 | 
| +#
 | 
| +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
 | 
| +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 | 
| +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 | 
| +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 | 
| +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 | 
| +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 | 
| +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
| +#
 | 
| +
 | 
| +__revision__ = "src/engine/SCons/Job.py 5134 2010/08/16 23:02:40 bdeegan"
 | 
| +
 | 
| +import SCons.compat
 | 
| +
 | 
| +import os
 | 
| +import signal
 | 
| +
 | 
| +import SCons.Errors
 | 
| +
 | 
| +# The default stack size (in kilobytes) of the threads used to execute
 | 
| +# jobs in parallel.
 | 
| +#
 | 
| +# We use a stack size of 256 kilobytes. The default on some platforms
 | 
| +# is too large and prevents us from creating enough threads to fully
 | 
| +# parallelized the build. For example, the default stack size on linux
 | 
| +# is 8 MBytes.
 | 
| +
 | 
| +explicit_stack_size = None
 | 
| +default_stack_size = 256
 | 
| +
 | 
| +interrupt_msg = 'Build interrupted.'
 | 
| +
 | 
| +
 | 
| +class InterruptState(object):
 | 
| +   def __init__(self):
 | 
| +       self.interrupted = False
 | 
| +
 | 
| +   def set(self):
 | 
| +       self.interrupted = True
 | 
| +
 | 
| +   def __call__(self):
 | 
| +       return self.interrupted
 | 
| +
 | 
| +
 | 
| +class Jobs(object):
 | 
| +    """An instance of this class initializes N jobs, and provides
 | 
| +    methods for starting, stopping, and waiting on all N jobs.
 | 
| +    """
 | 
| +
 | 
| +    def __init__(self, num, taskmaster):
 | 
| +        """
 | 
| +        create 'num' jobs using the given taskmaster.
 | 
| +
 | 
| +        If 'num' is 1 or less, then a serial job will be used,
 | 
| +        otherwise a parallel job with 'num' worker threads will
 | 
| +        be used.
 | 
| +
 | 
| +        The 'num_jobs' attribute will be set to the actual number of jobs
 | 
| +        allocated.  If more than one job is requested but the Parallel
 | 
| +        class can't do it, it gets reset to 1.  Wrapping interfaces that
 | 
| +        care should check the value of 'num_jobs' after initialization.
 | 
| +        """
 | 
| +
 | 
| +        self.job = None
 | 
| +        if num > 1:
 | 
| +            stack_size = explicit_stack_size
 | 
| +            if stack_size is None:
 | 
| +                stack_size = default_stack_size
 | 
| +                
 | 
| +            try:
 | 
| +                self.job = Parallel(taskmaster, num, stack_size)
 | 
| +                self.num_jobs = num
 | 
| +            except NameError:
 | 
| +                pass
 | 
| +        if self.job is None:
 | 
| +            self.job = Serial(taskmaster)
 | 
| +            self.num_jobs = 1
 | 
| +
 | 
| +    def run(self, postfunc=lambda: None):
 | 
| +        """Run the jobs.
 | 
| +
 | 
| +        postfunc() will be invoked after the jobs has run. It will be
 | 
| +        invoked even if the jobs are interrupted by a keyboard
 | 
| +        interrupt (well, in fact by a signal such as either SIGINT,
 | 
| +        SIGTERM or SIGHUP). The execution of postfunc() is protected
 | 
| +        against keyboard interrupts and is guaranteed to run to
 | 
| +        completion."""
 | 
| +        self._setup_sig_handler()
 | 
| +        try:
 | 
| +            self.job.start()
 | 
| +        finally:
 | 
| +            postfunc()
 | 
| +            self._reset_sig_handler()
 | 
| +
 | 
| +    def were_interrupted(self):
 | 
| +        """Returns whether the jobs were interrupted by a signal."""
 | 
| +        return self.job.interrupted()
 | 
| +
 | 
| +    def _setup_sig_handler(self):
 | 
| +        """Setup an interrupt handler so that SCons can shutdown cleanly in
 | 
| +        various conditions:
 | 
| +
 | 
| +          a) SIGINT: Keyboard interrupt
 | 
| +          b) SIGTERM: kill or system shutdown
 | 
| +          c) SIGHUP: Controlling shell exiting
 | 
| +
 | 
| +        We handle all of these cases by stopping the taskmaster. It
 | 
| +        turns out that it very difficult to stop the build process
 | 
| +        by throwing asynchronously an exception such as
 | 
| +        KeyboardInterrupt. For example, the python Condition
 | 
| +        variables (threading.Condition) and queue's do not seem to
 | 
| +        asynchronous-exception-safe. It would require adding a whole
 | 
| +        bunch of try/finally block and except KeyboardInterrupt all
 | 
| +        over the place.
 | 
| +
 | 
| +        Note also that we have to be careful to handle the case when
 | 
| +        SCons forks before executing another process. In that case, we
 | 
| +        want the child to exit immediately.
 | 
| +        """
 | 
| +        def handler(signum, stack, self=self, parentpid=os.getpid()):
 | 
| +            if os.getpid() == parentpid:
 | 
| +                self.job.taskmaster.stop()
 | 
| +                self.job.interrupted.set()
 | 
| +            else:
 | 
| +                os._exit(2)
 | 
| +
 | 
| +        self.old_sigint  = signal.signal(signal.SIGINT, handler)
 | 
| +        self.old_sigterm = signal.signal(signal.SIGTERM, handler)
 | 
| +        try:
 | 
| +            self.old_sighup = signal.signal(signal.SIGHUP, handler)
 | 
| +        except AttributeError:
 | 
| +            pass
 | 
| +
 | 
| +    def _reset_sig_handler(self):
 | 
| +        """Restore the signal handlers to their previous state (before the
 | 
| +         call to _setup_sig_handler()."""
 | 
| +
 | 
| +        signal.signal(signal.SIGINT, self.old_sigint)
 | 
| +        signal.signal(signal.SIGTERM, self.old_sigterm)
 | 
| +        try:
 | 
| +            signal.signal(signal.SIGHUP, self.old_sighup)
 | 
| +        except AttributeError:
 | 
| +            pass
 | 
| +
 | 
| +class Serial(object):
 | 
| +    """This class is used to execute tasks in series, and is more efficient
 | 
| +    than Parallel, but is only appropriate for non-parallel builds. Only
 | 
| +    one instance of this class should be in existence at a time.
 | 
| +
 | 
| +    This class is not thread safe.
 | 
| +    """
 | 
| +
 | 
| +    def __init__(self, taskmaster):
 | 
| +        """Create a new serial job given a taskmaster. 
 | 
| +
 | 
| +        The taskmaster's next_task() method should return the next task
 | 
| +        that needs to be executed, or None if there are no more tasks. The
 | 
| +        taskmaster's executed() method will be called for each task when it
 | 
| +        is successfully executed or failed() will be called if it failed to
 | 
| +        execute (e.g. execute() raised an exception)."""
 | 
| +        
 | 
| +        self.taskmaster = taskmaster
 | 
| +        self.interrupted = InterruptState()
 | 
| +
 | 
| +    def start(self):
 | 
| +        """Start the job. This will begin pulling tasks from the taskmaster
 | 
| +        and executing them, and return when there are no more tasks. If a task
 | 
| +        fails to execute (i.e. execute() raises an exception), then the job will
 | 
| +        stop."""
 | 
| +        
 | 
| +        while True:
 | 
| +            task = self.taskmaster.next_task()
 | 
| +
 | 
| +            if task is None:
 | 
| +                break
 | 
| +
 | 
| +            try:
 | 
| +                task.prepare()
 | 
| +                if task.needs_execute():
 | 
| +                    task.execute()
 | 
| +            except:
 | 
| +                if self.interrupted():
 | 
| +                    try:
 | 
| +                        raise SCons.Errors.BuildError(
 | 
| +                            task.targets[0], errstr=interrupt_msg)
 | 
| +                    except:
 | 
| +                        task.exception_set()
 | 
| +                else:
 | 
| +                    task.exception_set()
 | 
| +
 | 
| +                # Let the failed() callback function arrange for the
 | 
| +                # build to stop if that's appropriate.
 | 
| +                task.failed()
 | 
| +            else:
 | 
| +                task.executed()
 | 
| +
 | 
| +            task.postprocess()
 | 
| +        self.taskmaster.cleanup()
 | 
| +
 | 
| +
 | 
| +# Trap import failure so that everything in the Job module but the
 | 
| +# Parallel class (and its dependent classes) will work if the interpreter
 | 
| +# doesn't support threads.
 | 
| +try:
 | 
| +    import queue
 | 
| +    import threading
 | 
| +except ImportError:
 | 
| +    pass
 | 
| +else:
 | 
| +    class Worker(threading.Thread):
 | 
| +        """A worker thread waits on a task to be posted to its request queue,
 | 
| +        dequeues the task, executes it, and posts a tuple including the task
 | 
| +        and a boolean indicating whether the task executed successfully. """
 | 
| +
 | 
| +        def __init__(self, requestQueue, resultsQueue, interrupted):
 | 
| +            threading.Thread.__init__(self)
 | 
| +            self.setDaemon(1)
 | 
| +            self.requestQueue = requestQueue
 | 
| +            self.resultsQueue = resultsQueue
 | 
| +            self.interrupted = interrupted
 | 
| +            self.start()
 | 
| +
 | 
| +        def run(self):
 | 
| +            while True:
 | 
| +                task = self.requestQueue.get()
 | 
| +
 | 
| +                if task is None:
 | 
| +                    # The "None" value is used as a sentinel by
 | 
| +                    # ThreadPool.cleanup().  This indicates that there
 | 
| +                    # are no more tasks, so we should quit.
 | 
| +                    break
 | 
| +
 | 
| +                try:
 | 
| +                    if self.interrupted():
 | 
| +                        raise SCons.Errors.BuildError(
 | 
| +                            task.targets[0], errstr=interrupt_msg)
 | 
| +                    task.execute()
 | 
| +                except:
 | 
| +                    task.exception_set()
 | 
| +                    ok = False
 | 
| +                else:
 | 
| +                    ok = True
 | 
| +
 | 
| +                self.resultsQueue.put((task, ok))
 | 
| +
 | 
| +    class ThreadPool(object):
 | 
| +        """This class is responsible for spawning and managing worker threads."""
 | 
| +
 | 
| +        def __init__(self, num, stack_size, interrupted):
 | 
| +            """Create the request and reply queues, and 'num' worker threads.
 | 
| +            
 | 
| +            One must specify the stack size of the worker threads. The
 | 
| +            stack size is specified in kilobytes.
 | 
| +            """
 | 
| +            self.requestQueue = queue.Queue(0)
 | 
| +            self.resultsQueue = queue.Queue(0)
 | 
| +
 | 
| +            try:
 | 
| +                prev_size = threading.stack_size(stack_size*1024) 
 | 
| +            except AttributeError, e:
 | 
| +                # Only print a warning if the stack size has been
 | 
| +                # explicitly set.
 | 
| +                if not explicit_stack_size is None:
 | 
| +                    msg = "Setting stack size is unsupported by this version of Python:\n    " + \
 | 
| +                        e.args[0]
 | 
| +                    SCons.Warnings.warn(SCons.Warnings.StackSizeWarning, msg)
 | 
| +            except ValueError, e:
 | 
| +                msg = "Setting stack size failed:\n    " + str(e)
 | 
| +                SCons.Warnings.warn(SCons.Warnings.StackSizeWarning, msg)
 | 
| +
 | 
| +            # Create worker threads
 | 
| +            self.workers = []
 | 
| +            for _ in range(num):
 | 
| +                worker = Worker(self.requestQueue, self.resultsQueue, interrupted)
 | 
| +                self.workers.append(worker)
 | 
| +
 | 
| +            if 'prev_size' in locals():
 | 
| +                threading.stack_size(prev_size)
 | 
| +
 | 
| +        def put(self, task):
 | 
| +            """Put task into request queue."""
 | 
| +            self.requestQueue.put(task)
 | 
| +
 | 
| +        def get(self):
 | 
| +            """Remove and return a result tuple from the results queue."""
 | 
| +            return self.resultsQueue.get()
 | 
| +
 | 
| +        def preparation_failed(self, task):
 | 
| +            self.resultsQueue.put((task, False))
 | 
| +
 | 
| +        def cleanup(self):
 | 
| +            """
 | 
| +            Shuts down the thread pool, giving each worker thread a
 | 
| +            chance to shut down gracefully.
 | 
| +            """
 | 
| +            # For each worker thread, put a sentinel "None" value
 | 
| +            # on the requestQueue (indicating that there's no work
 | 
| +            # to be done) so that each worker thread will get one and
 | 
| +            # terminate gracefully.
 | 
| +            for _ in self.workers:
 | 
| +                self.requestQueue.put(None)
 | 
| +
 | 
| +            # Wait for all of the workers to terminate.
 | 
| +            # 
 | 
| +            # If we don't do this, later Python versions (2.4, 2.5) often
 | 
| +            # seem to raise exceptions during shutdown.  This happens
 | 
| +            # in requestQueue.get(), as an assertion failure that
 | 
| +            # requestQueue.not_full is notified while not acquired,
 | 
| +            # seemingly because the main thread has shut down (or is
 | 
| +            # in the process of doing so) while the workers are still
 | 
| +            # trying to pull sentinels off the requestQueue.
 | 
| +            #
 | 
| +            # Normally these terminations should happen fairly quickly,
 | 
| +            # but we'll stick a one-second timeout on here just in case
 | 
| +            # someone gets hung.
 | 
| +            for worker in self.workers:
 | 
| +                worker.join(1.0)
 | 
| +            self.workers = []
 | 
| +
 | 
| +    class Parallel(object):
 | 
| +        """This class is used to execute tasks in parallel, and is somewhat 
 | 
| +        less efficient than Serial, but is appropriate for parallel builds.
 | 
| +
 | 
| +        This class is thread safe.
 | 
| +        """
 | 
| +
 | 
| +        def __init__(self, taskmaster, num, stack_size):
 | 
| +            """Create a new parallel job given a taskmaster.
 | 
| +
 | 
| +            The taskmaster's next_task() method should return the next
 | 
| +            task that needs to be executed, or None if there are no more
 | 
| +            tasks. The taskmaster's executed() method will be called
 | 
| +            for each task when it is successfully executed or failed()
 | 
| +            will be called if the task failed to execute (i.e. execute()
 | 
| +            raised an exception).
 | 
| +
 | 
| +            Note: calls to taskmaster are serialized, but calls to
 | 
| +            execute() on distinct tasks are not serialized, because
 | 
| +            that is the whole point of parallel jobs: they can execute
 | 
| +            multiple tasks simultaneously. """
 | 
| +
 | 
| +            self.taskmaster = taskmaster
 | 
| +            self.interrupted = InterruptState()
 | 
| +            self.tp = ThreadPool(num, stack_size, self.interrupted)
 | 
| +
 | 
| +            self.maxjobs = num
 | 
| +
 | 
| +        def start(self):
 | 
| +            """Start the job. This will begin pulling tasks from the
 | 
| +            taskmaster and executing them, and return when there are no
 | 
| +            more tasks. If a task fails to execute (i.e. execute() raises
 | 
| +            an exception), then the job will stop."""
 | 
| +
 | 
| +            jobs = 0
 | 
| +            
 | 
| +            while True:
 | 
| +                # Start up as many available tasks as we're
 | 
| +                # allowed to.
 | 
| +                while jobs < self.maxjobs:
 | 
| +                    task = self.taskmaster.next_task()
 | 
| +                    if task is None:
 | 
| +                        break
 | 
| +
 | 
| +                    try:
 | 
| +                        # prepare task for execution
 | 
| +                        task.prepare()
 | 
| +                    except:
 | 
| +                        task.exception_set()
 | 
| +                        task.failed()
 | 
| +                        task.postprocess()
 | 
| +                    else:
 | 
| +                        if task.needs_execute():
 | 
| +                            # dispatch task
 | 
| +                            self.tp.put(task)
 | 
| +                            jobs = jobs + 1
 | 
| +                        else:
 | 
| +                            task.executed()
 | 
| +                            task.postprocess()
 | 
| +
 | 
| +                if not task and not jobs: break
 | 
| +
 | 
| +                # Let any/all completed tasks finish up before we go
 | 
| +                # back and put the next batch of tasks on the queue.
 | 
| +                while True:
 | 
| +                    task, ok = self.tp.get()
 | 
| +                    jobs = jobs - 1
 | 
| +
 | 
| +                    if ok:
 | 
| +                        task.executed()
 | 
| +                    else:
 | 
| +                        if self.interrupted():
 | 
| +                            try:
 | 
| +                                raise SCons.Errors.BuildError(
 | 
| +                                    task.targets[0], errstr=interrupt_msg)
 | 
| +                            except:
 | 
| +                                task.exception_set()
 | 
| +
 | 
| +                        # Let the failed() callback function arrange
 | 
| +                        # for the build to stop if that's appropriate.
 | 
| +                        task.failed()
 | 
| +
 | 
| +                    task.postprocess()
 | 
| +
 | 
| +                    if self.tp.resultsQueue.empty():
 | 
| +                        break
 | 
| +
 | 
| +            self.tp.cleanup()
 | 
| +            self.taskmaster.cleanup()
 | 
| +
 | 
| +# Local Variables:
 | 
| +# tab-width:4
 | 
| +# indent-tabs-mode:nil
 | 
| +# End:
 | 
| +# vim: set expandtab tabstop=4 shiftwidth=4:
 | 
| 
 | 
| Property changes on: scons-2.0.1/engine/SCons/Job.py
 | 
| ___________________________________________________________________
 | 
| Added: svn:eol-style
 | 
|    + LF
 | 
| 
 | 
| 
 |