__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+import time
+
class Jobs:
"""An instance of this class initializes N jobs, and provides
methods for starting, stopping, and waiting on all N jobs.
otherwise 'num' parallel jobs will be used.
"""
+ # Keeps track of keyboard interrupts:
+ self.keyboard_interrupt = 0
+
if num > 1:
self.jobs = []
for i in range(num):
self.jobs.append(Parallel(taskmaster, self))
else:
- self.jobs = [Serial(taskmaster)]
-
- def start(self):
- """start the jobs"""
+ self.jobs = [Serial(taskmaster, self)]
+
+ self.running = []
- for job in self.jobs:
- job.start()
+ def run(self):
+ """run the jobs, and wait for them to finish"""
- def wait(self):
- """ wait for the jobs started with start() to finish"""
-
- for job in self.jobs:
- job.wait()
-
- def stop(self):
- """
- stop the jobs started with start()
+ try:
+ for job in self.jobs:
+ job.start()
+ self.running.append(job)
+ while self.running:
+ self.running[-1].wait()
+ self.running.pop()
+ except KeyboardInterrupt:
+ # mask any further keyboard interrupts so that scons
+ # can shutdown cleanly:
+ # (this only masks the keyboard interrupt for Python,
+ # child processes can still get the keyboard interrupt)
+ import signal
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+ for job in self.running:
+ job.keyboard_interrupt()
+ else:
+ self.keyboard_interrupt = 1
- This function does not wait for the jobs to finish.
- """
+ # wait on any remaining jobs to finish:
+ for job in self.running:
+ job.wait()
- for job in self.jobs:
- job.stop()
+ if self.keyboard_interrupt:
+ raise KeyboardInterrupt
class Serial:
"""This class is used to execute tasks in series, and is more efficient
This class is not thread safe.
"""
- def __init__(self, taskmaster):
+ def __init__(self, taskmaster, jobs):
"""Create a new serial job given a taskmaster.
The taskmaster's next_task() method should return the next task
is_blocked() method will not be called. """
self.taskmaster = taskmaster
+ self.jobs = jobs
def start(self):
fails to execute (i.e. execute() raises an exception), then the job will
stop."""
- while 1:
+ while not self.jobs.keyboard_interrupt:
task = self.taskmaster.next_task()
if task is None:
try:
task.execute()
+ except KeyboardInterrupt:
+ raise
except:
# Let the failed() callback function arrange for the
# build to stop if that's appropriate.
else:
task.executed()
- def stop(self):
- """Serial jobs are always finished when start() returns, so there
- is nothing to do here"""
-
- pass
-
def wait(self):
"""Serial jobs are always finished when start() returns, so there
is nothing to do here"""
pass
+
+ def keyboard_interrupt(self):
+ self.jobs.keyboard_interrupt = 1
# The will hold a condition variable once the first parallel task
self.taskmaster = taskmaster
self.jobs = jobs
self.thread = threading.Thread(None, self.__run)
- self.stop_running = 0
if cv is None:
cv = threading.Condition()
tasks from the task master and executing them. This method returns
immediately and doesn't wait for the jobs to be executed.
- If a task fails to execute (i.e. execute() raises an exception),
- all jobs will be stopped.
-
- To stop the job, call stop().
To wait for the job to finish, call wait().
"""
self.thread.start()
- def stop(self):
- """Stop the job. This will cause the job to finish after the
- currently executing task is done. A job that has been stopped can
- not be restarted.
+ def wait(self):
+ """Wait for the job to finish. A job is finished when there
+ are no more tasks.
- To wait for the job to finish, call wait().
+ This method should only be called after start() has been called.
"""
+ # Sleeping in a loop like this is lame. Calling
+ # self.thread.join() would be much nicer, but
+ # on Linux self.thread.join() doesn't always
+ # return when a KeyboardInterrupt happens, and when
+ # it does return, it causes Python to hang on shutdown.
+ # In other words this is just
+ # a work-around for some bugs/limitations in the
+ # self.thread.join() method.
+ while self.thread.isAlive():
+ time.sleep(0.5)
+
+ def keyboard_interrupt(self):
cv.acquire()
- self.stop_running = 1
- # wake up the sleeping jobs so this job will end as soon as possible:
- cv.notifyAll()
+ self.jobs.keyboard_interrupt = 1
+ cv.notifyAll()
cv.release()
-
- def wait(self):
- """Wait for the job to finish. A job is finished when either there
- are no more tasks or the job has been stopped and it is no longer
- executing a task.
-
- This method should only be called after start() has been called.
-
- To stop the job, call stop().
- """
- self.thread.join()
def __run(self):
"""private method that actually executes the tasks"""
try:
while 1:
- while self.taskmaster.is_blocked() and not self.stop_running:
+ while (self.taskmaster.is_blocked() and
+ not self.jobs.keyboard_interrupt):
cv.wait(None)
- # check this before calling next_task(), because
- # this job may have been stopped because of a build
- # failure:
- if self.stop_running:
+ if self.jobs.keyboard_interrupt:
break
task = self.taskmaster.next_task()
task.execute()
finally:
cv.acquire()
+ except KeyboardInterrupt:
+ self.jobs.keyboard_interrupt = 1
except:
# Let the failed() callback function arrange for
# calling self.jobs.stop() to to stop the build
# signal the cv whether the task failed or not,
# or otherwise the other Jobs might
# remain blocked:
- if not self.taskmaster.is_blocked():
+ if (not self.taskmaster.is_blocked() or
+ self.jobs.keyboard_interrupt):
cv.notifyAll()
finally: