From 72e3615b951a91dfa7e4eb48e147486607b74938 Mon Sep 17 00:00:00 2001 From: Steven Knight Date: Sat, 18 May 2002 05:36:40 +0000 Subject: Ctrl-C Improvements (Anthony Roach) --- etc/TestCmd.py | 10 ++- src/CHANGES.txt | 3 + src/engine/SCons/Job.py | 124 +++++++++++++++++++----------------- src/engine/SCons/JobTests.py | 12 ++-- src/engine/SCons/Node/NodeTests.py | 2 +- src/engine/SCons/Node/__init__.py | 2 + src/engine/SCons/Script/__init__.py | 5 +- 7 files changed, 87 insertions(+), 71 deletions(-) diff --git a/etc/TestCmd.py b/etc/TestCmd.py index 06e324f..17d520b 100644 --- a/etc/TestCmd.py +++ b/etc/TestCmd.py @@ -510,7 +510,10 @@ class TestCmd: elif run < 0: run = len(self._stderr) + run run = run - 1 - return self._stderr[run] + if run >= len(self._stderr) or run < 0: + return '' + else: + return self._stderr[run] def stdout(self, run = None): """Returns the standard output from the specified run number. @@ -524,7 +527,10 @@ class TestCmd: elif run < 0: run = len(self._stdout) + run run = run - 1 - return self._stdout[run] + if run >= len(self._stdout) or run < 0: + return '' + else: + return self._stdout[run] def subdir(self, *subdirs): """Create new subdirectories under the temporary working diff --git a/src/CHANGES.txt b/src/CHANGES.txt index 6274991..beb47d7 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -46,6 +46,9 @@ RELEASE 0.08 - - Fall back to importing the SCons.TimeStamp module if the SCons.MD5 module can't be imported. + - Fix interrupt handling to guarantee that a single interrupt will + halt SCons both when using -j and not. + From Zed Shaw: - Add an Append() method to Environments, to append values to diff --git a/src/engine/SCons/Job.py b/src/engine/SCons/Job.py index 1eb5fa0..21f4617 100644 --- a/src/engine/SCons/Job.py +++ b/src/engine/SCons/Job.py @@ -31,6 +31,8 @@ stop, and wait on jobs. __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. @@ -44,34 +46,47 @@ class 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 @@ -81,7 +96,7 @@ class Serial: 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 @@ -92,6 +107,7 @@ class Serial: is_blocked() method will not be called. """ self.taskmaster = taskmaster + self.jobs = jobs def start(self): @@ -100,7 +116,7 @@ class Serial: 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: @@ -108,6 +124,8 @@ class Serial: try: task.execute() + except KeyboardInterrupt: + raise except: # Let the failed() callback function arrange for the # build to stop if that's appropriate. @@ -115,16 +133,13 @@ class Serial: 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 @@ -171,7 +186,6 @@ class Parallel: self.taskmaster = taskmaster self.jobs = jobs self.thread = threading.Thread(None, self.__run) - self.stop_running = 0 if cv is None: cv = threading.Condition() @@ -181,38 +195,33 @@ class Parallel: 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""" @@ -222,13 +231,11 @@ class Parallel: 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() @@ -242,6 +249,8 @@ class Parallel: 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 @@ -253,7 +262,8 @@ class Parallel: # 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: diff --git a/src/engine/SCons/JobTests.py b/src/engine/SCons/JobTests.py index f7c180c..55e5f0f 100644 --- a/src/engine/SCons/JobTests.py +++ b/src/engine/SCons/JobTests.py @@ -172,8 +172,7 @@ class ParallelTestCase(unittest.TestCase): taskmaster = Taskmaster(num_tasks, self, Task) jobs = SCons.Job.Jobs(num_jobs, taskmaster) - jobs.start() - jobs.wait() + jobs.run() self.failUnless(not taskmaster.tasks_were_serial(), "the tasks were not executed in parallel") @@ -190,8 +189,7 @@ class SerialTestCase(unittest.TestCase): taskmaster = Taskmaster(num_tasks, self, Task) jobs = SCons.Job.Jobs(1, taskmaster) - jobs.start() - jobs.wait() + jobs.run() self.failUnless(taskmaster.tasks_were_serial(), "the tasks were not executed in series") @@ -208,8 +206,7 @@ class SerialExceptionTestCase(unittest.TestCase): taskmaster = Taskmaster(num_tasks, self, ExceptionTask) jobs = SCons.Job.Jobs(1, taskmaster) - jobs.start() - jobs.wait() + jobs.run() self.failIf(taskmaster.num_executed, "a task was executed") @@ -224,8 +221,7 @@ class ParallelExceptionTestCase(unittest.TestCase): taskmaster = Taskmaster(num_tasks, self, ExceptionTask) jobs = SCons.Job.Jobs(num_jobs, taskmaster) - jobs.start() - jobs.wait() + jobs.run() self.failIf(taskmaster.num_executed, "a task was executed") diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py index fac20e3..b67d352 100644 --- a/src/engine/SCons/Node/NodeTests.py +++ b/src/engine/SCons/Node/NodeTests.py @@ -570,7 +570,7 @@ class NodeTestCase(unittest.TestCase): assert nodes[0].name == u"Util.py" assert nodes[1].name == u"UtilTests.py" \n""" - exec code + exec code in globals(), locals() nodes = SCons.Node.arg2nodes(["Util.py", "UtilTests.py"], Factory) assert len(nodes) == 2, nodes diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 472ec07..e440060 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -114,6 +114,8 @@ class Node: try: stat = apply(self.builder.execute, (), self.generate_build_args()) + except KeyboardInterrupt: + raise except: raise BuildError(self, "Exception", sys.exc_type, diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py index d0b17d4..2fbde13 100644 --- a/src/engine/SCons/Script/__init__.py +++ b/src/engine/SCons/Script/__init__.py @@ -870,8 +870,7 @@ def _main(): jobs = SCons.Job.Jobs(num_jobs, taskmaster) try: - jobs.start() - jobs.wait() + jobs.run() finally: SCons.Sig.write() @@ -882,7 +881,7 @@ def main(): pass except KeyboardInterrupt: print "Build interrupted." - sys.exit(1) + sys.exit(2) except SyntaxError, e: _scons_syntax_error(e) except UserError, e: -- cgit v0.12